Add WindowParser and refactor theme and brightness logic

Introduced `WindowParser` to parse `windowN` segments for PIP/PBP
capabilities in MCCS strings. Added new data models (`WindowCapability`,
`WindowArea`, `WindowSize`) to represent parsed window data. Updated
`MccsCapabilitiesParser` to handle `windowN` segments and added unit
tests for various configurations.

Refactored brightness control in `DdcCiController` to exclusively use
VCP code `0x10`, removing high-level API methods. Updated `DdcCiNative`
to streamline brightness operations.

Revised `LightSwitchListener` and `LightSwitchStateManager` to use
separate light/dark theme events, eliminating race conditions. Removed
registry-based theme detection logic.

Enhanced `VcpCodeNames` with additional VCP codes and improved
categorization. Updated documentation and architecture diagrams to
reflect these changes. Removed unused legacy methods and improved
logging and error handling.
This commit is contained in:
Yu Leng
2025-12-04 05:44:59 +08:00
parent d9584de585
commit f32ee3ea02
13 changed files with 1681 additions and 252 deletions

View File

@@ -42,8 +42,12 @@ MccsCapabilitiesParser (main parser)
├── VcpEntryParser (sub-parser for vcp() content)
│ └── TryParseEntry() → VcpEntry
── VcpNameParser (sub-parser for vcpname() content)
└── TryParseEntry() → (byte code, string name)
── VcpNameParser (sub-parser for vcpname() content)
└── TryParseEntry() → (byte code, string name)
└── WindowParser (sub-parser for windowN() content)
├── Parse() → WindowCapability
└── ParseSubSegment() → (name, content)?
```
### Design Principles
@@ -146,6 +150,25 @@ Examples:
| `vcp(...)` | VCP code entries | VcpEntryParser |
| `mccs_ver(...)` | MCCS version | Direct assignment |
| `vcpname(...)` | Custom VCP names | VcpNameParser |
| `windowN(...)` | PIP/PBP window capabilities | WindowParser |
### Window Segment Format
The `windowN` segment (where N is 1, 2, 3, etc.) describes PIP/PBP window capabilities:
```
window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10))
```
| Sub-field | Format | Description |
|-----------|--------|-------------|
| `type` | `type(PIP)` or `type(PBP)` | Window type (Picture-in-Picture or Picture-by-Picture) |
| `area` | `area(x1 y1 x2 y2)` | Window area coordinates in pixels |
| `max` | `max(width height)` | Maximum window dimensions |
| `min` | `min(width height)` | Minimum window dimensions |
| `window` | `window(id)` | Window identifier |
All sub-fields are optional; missing fields default to zero values.
## Error Handling
@@ -195,10 +218,3 @@ if (result.HasErrors)
3. **Nested parentheses** in VCP values
4. **Unknown segments** (logged but not fatal)
5. **Malformed input** (partial results returned)
## Future Extensions
- Add `edid()` segment parsing
- Add `window()` segment parsing
- Add validation levels (VALID/USABLE/INVALID like ddcutil)
- Add source generation for AOT compatibility

File diff suppressed because it is too large Load Diff

View File

@@ -255,20 +255,25 @@ void LightSwitchStateManager::NotifyPowerDisplay(bool isLight)
try
{
// Signal PowerDisplay to check LightSwitch settings and apply appropriate profile
// PowerDisplay will read LightSwitch settings to determine which profile to apply
// Signal PowerDisplay with the specific theme event
// Using separate events for light/dark eliminates race conditions where PowerDisplay
// might read the registry before LightSwitch has finished updating it
const wchar_t* eventName = isLight
? L"Local\\PowerToys_LightSwitch_LightTheme"
: L"Local\\PowerToys_LightSwitch_DarkTheme";
Logger::info(L"[LightSwitchStateManager] Notifying PowerDisplay about theme change (isLight: {})", isLight);
HANDLE hThemeChangedEvent = CreateEventW(nullptr, FALSE, FALSE, L"Local\\PowerToys_LightSwitch_ThemeChanged");
if (hThemeChangedEvent)
HANDLE hThemeEvent = CreateEventW(nullptr, FALSE, FALSE, eventName);
if (hThemeEvent)
{
SetEvent(hThemeChangedEvent);
CloseHandle(hThemeChangedEvent);
Logger::info(L"[LightSwitchStateManager] Theme change event signaled to PowerDisplay");
SetEvent(hThemeEvent);
CloseHandle(hThemeEvent);
Logger::info(L"[LightSwitchStateManager] Theme event signaled to PowerDisplay: {}", eventName);
}
else
{
Logger::warn(L"[LightSwitchStateManager] Failed to create theme change event (error: {})", GetLastError());
Logger::warn(L"[LightSwitchStateManager] Failed to create theme event (error: {})", GetLastError());
}
}
catch (...)

View File

@@ -584,4 +584,158 @@ public class MccsCapabilitiesParserTests
// Assert
Assert.AreEqual(SimpleCapabilities, result.Capabilities.Raw);
}
[TestMethod]
public void Parse_Window1Segment_ParsesCorrectly()
{
// Arrange - Full window segment with all fields
var input = "(vcp(10)window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
var window = result.Capabilities.Windows[0];
Assert.AreEqual(1, window.WindowNumber);
Assert.AreEqual("PIP", window.Type);
Assert.AreEqual(25, window.Area.X1);
Assert.AreEqual(25, window.Area.Y1);
Assert.AreEqual(1895, window.Area.X2);
Assert.AreEqual(1175, window.Area.Y2);
Assert.AreEqual(640, window.MaxSize.Width);
Assert.AreEqual(480, window.MaxSize.Height);
Assert.AreEqual(10, window.MinSize.Width);
Assert.AreEqual(10, window.MinSize.Height);
Assert.AreEqual(10, window.WindowId);
}
[TestMethod]
public void Parse_MultipleWindows_ParsesAll()
{
// Arrange - Two windows (PIP and PBP)
var input = "(window1(type(PIP) area(0 0 640 480))window2(type(PBP) area(640 0 1280 480)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(2, result.Capabilities.Windows.Count);
var window1 = result.Capabilities.Windows[0];
Assert.AreEqual(1, window1.WindowNumber);
Assert.AreEqual("PIP", window1.Type);
Assert.AreEqual(0, window1.Area.X1);
Assert.AreEqual(640, window1.Area.X2);
var window2 = result.Capabilities.Windows[1];
Assert.AreEqual(2, window2.WindowNumber);
Assert.AreEqual("PBP", window2.Type);
Assert.AreEqual(640, window2.Area.X1);
Assert.AreEqual(1280, window2.Area.X2);
}
[TestMethod]
public void Parse_WindowWithMissingFields_HandlesGracefully()
{
// Arrange - Window with only type and area (missing max, min, window)
var input = "(window1(type(PIP) area(0 0 640 480)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
var window = result.Capabilities.Windows[0];
Assert.AreEqual(1, window.WindowNumber);
Assert.AreEqual("PIP", window.Type);
Assert.AreEqual(640, window.Area.X2);
Assert.AreEqual(480, window.Area.Y2);
// Default values for missing fields
Assert.AreEqual(0, window.MaxSize.Width);
Assert.AreEqual(0, window.MinSize.Width);
Assert.AreEqual(0, window.WindowId);
}
[TestMethod]
public void Parse_WindowWithOnlyType_ParsesType()
{
// Arrange
var input = "(window1(type(PBP)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
Assert.AreEqual("PBP", result.Capabilities.Windows[0].Type);
}
[TestMethod]
public void Parse_NoWindowSegment_HasWindowSupportFalse()
{
// Arrange
var input = "(prot(monitor)vcp(10 12))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsFalse(result.Capabilities.HasWindowSupport);
Assert.AreEqual(0, result.Capabilities.Windows.Count);
}
[TestMethod]
public void Parse_WindowAreaDimensions_CalculatesCorrectly()
{
// Arrange
var input = "(window1(area(100 200 500 600)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
var area = result.Capabilities.Windows[0].Area;
Assert.AreEqual(400, area.Width); // 500 - 100
Assert.AreEqual(400, area.Height); // 600 - 200
}
[TestMethod]
public void Parse_RealWorldMccsWindowExample_ParsesCorrectly()
{
// Arrange - Example from MCCS 2.2a specification
var input = "(prot(display)type(lcd)model(PD3220U)cmds(01 02 03)vcp(10 12)mccs_ver(2.2)window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual("PD3220U", result.Capabilities.Model);
Assert.AreEqual("2.2", result.Capabilities.MccsVersion);
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual("PIP", result.Capabilities.Windows[0].Type);
}
[TestMethod]
public void Parse_WindowWithExtraSpaces_HandlesCorrectly()
{
// Arrange - Extra spaces in content
var input = "(window1( type( PIP ) area( 0 0 640 480 ) ))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual("PIP", result.Capabilities.Windows[0].Type);
Assert.AreEqual(640, result.Capabilities.Windows[0].Area.X2);
}
}

View File

@@ -96,57 +96,8 @@ namespace PowerDisplay.Common.Drivers.DDC
/// <summary>
/// Set monitor brightness using VCP code 0x10
/// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
brightness = Math.Clamp(brightness, 0, 100);
return await Task.Run(
() =>
{
var physicalHandle = GetPhysicalHandle(monitor);
if (physicalHandle == IntPtr.Zero)
{
return MonitorOperationResult.Failure("No physical handle found");
}
try
{
var currentInfo = GetBrightnessInfoCore(monitor.Id, physicalHandle);
if (!currentInfo.IsValid)
{
Logger.LogWarning($"[{monitor.Id}] Cannot read current brightness");
return MonitorOperationResult.Failure("Cannot read current brightness");
}
uint targetValue = (uint)currentInfo.FromPercentage(brightness);
// First try high-level API
if (DdcCiNative.TrySetMonitorBrightness(physicalHandle, targetValue))
{
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via high-level API");
return MonitorOperationResult.Success();
}
// Try VCP code 0x10 (standard brightness)
if (DdcCiNative.TrySetVCPFeature(physicalHandle, VcpCodeBrightness, targetValue))
{
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via 0x10");
return MonitorOperationResult.Success();
}
var lastError = GetLastError();
Logger.LogError($"[{monitor.Id}] Failed to set brightness, error: {lastError}");
return MonitorOperationResult.Failure($"Failed to set brightness via DDC/CI", (int)lastError);
}
catch (Exception ex)
{
Logger.LogError($"[{monitor.Id}] Exception setting brightness: {ex.Message}");
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
}
},
cancellationToken);
}
public Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeBrightness, brightness, 0, 100, cancellationToken);
/// <summary>
/// Set monitor contrast
@@ -785,8 +736,7 @@ namespace PowerDisplay.Common.Drivers.DDC
}
/// <summary>
/// Core implementation for getting brightness information using high-level API or VCP code 0x10.
/// Used by both GetBrightnessAsync and SetBrightnessAsync.
/// Core implementation for getting brightness information using VCP code 0x10.
/// </summary>
/// <param name="monitorId">Monitor ID for logging.</param>
/// <param name="physicalHandle">Physical monitor handle.</param>
@@ -799,15 +749,7 @@ namespace PowerDisplay.Common.Drivers.DDC
return BrightnessInfo.Invalid;
}
// First try high-level API
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint min, out uint current, out uint max))
{
Logger.LogDebug($"[{monitorId}] Brightness via high-level API: {current}/{max}");
return new BrightnessInfo((int)current, (int)min, (int)max);
}
// Try VCP code 0x10 (standard brightness)
if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out current, out max))
if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out uint current, out uint max))
{
Logger.LogDebug($"[{monitorId}] Brightness via VCP 0x10: {current}/{max}");
return new BrightnessInfo((int)current, 0, (int)max);

View File

@@ -159,60 +159,6 @@ namespace PowerDisplay.Common.Drivers.DDC
}
}
/// <summary>
/// Safe wrapper for getting advanced brightness information
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <param name="minBrightness">Minimum brightness</param>
/// <param name="currentBrightness">Current brightness</param>
/// <param name="maxBrightness">Maximum brightness</param>
/// <returns>True if successful</returns>
public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)
{
minBrightness = 0;
currentBrightness = 0;
maxBrightness = 0;
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return GetMonitorBrightness(hPhysicalMonitor, out minBrightness, out currentBrightness, out maxBrightness);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
Logger.LogDebug($"TryGetMonitorBrightness failed: {ex.Message}");
return false;
}
}
/// <summary>
/// Safe wrapper for setting monitor brightness
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <param name="brightness">Brightness value</param>
/// <returns>True if successful</returns>
public static bool TrySetMonitorBrightness(IntPtr hPhysicalMonitor, uint brightness)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return SetMonitorBrightness(hPhysicalMonitor, brightness);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
Logger.LogDebug($"TrySetMonitorBrightness failed: {ex.Message}");
return false;
}
}
/// <summary>
/// Fetches VCP capabilities string from a monitor and returns a validation result.
/// This is the slow I2C operation (~4 seconds per monitor) that should only be done once.
@@ -290,8 +236,8 @@ namespace PowerDisplay.Common.Drivers.DDC
try
{
// Try a quick brightness read to verify connection
return TryGetMonitorBrightness(hPhysicalMonitor, out _, out _, out _);
// Try a quick brightness read via VCP 0x10 to verify connection
return TryGetVCPFeature(hPhysicalMonitor, NativeConstants.VcpCodeBrightness, out _, out _);
}
catch
{

View File

@@ -203,18 +203,11 @@ namespace PowerDisplay.Common.Drivers.DDC
}
/// <summary>
/// Get current brightness using VCP code 0x10 only
/// Get current brightness using VCP code 0x10
/// </summary>
private BrightnessInfo GetCurrentBrightness(IntPtr handle)
{
// Try high-level API first
if (DdcCiNative.TryGetMonitorBrightness(handle, out uint min, out uint current, out uint max))
{
return new BrightnessInfo((int)current, (int)min, (int)max);
}
// Try VCP code 0x10 (standard brightness)
if (DdcCiNative.TryGetVCPFeature(handle, VcpCodeBrightness, out current, out max))
if (DdcCiNative.TryGetVCPFeature(handle, VcpCodeBrightness, out uint current, out uint max))
{
return new BrightnessInfo((int)current, 0, (int)max);
}

View File

@@ -140,41 +140,6 @@ namespace PowerDisplay.Common.Drivers
IntPtr pszASCIICapabilitiesString,
uint dwCapabilitiesStringLengthInCharacters);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetMonitorBrightness(
IntPtr hPhysicalMonitor,
out uint pdwMinimumBrightness,
out uint pdwCurrentBrightness,
out uint pdwMaximumBrightness);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetMonitorBrightness(
IntPtr hPhysicalMonitor,
uint dwNewBrightness);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetMonitorContrast(
IntPtr hPhysicalMonitor,
out uint pdwMinimumContrast,
out uint pdwCurrentContrast,
out uint pdwMaximumContrast);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetMonitorContrast(
IntPtr hPhysicalMonitor,
uint dwNewContrast);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetMonitorCapabilities(
IntPtr hPhysicalMonitor,
out uint pdwMonitorCapabilities,
out uint pdwSupportedColorTemperatures);
// ==================== Kernel32.dll ====================
[LibraryImport("kernel32.dll")]
internal static partial uint GetLastError();

View File

@@ -47,6 +47,16 @@ namespace PowerDisplay.Common.Models
/// </summary>
public Dictionary<byte, VcpCodeInfo> SupportedVcpCodes { get; set; } = new();
/// <summary>
/// Window capabilities for PIP/PBP support
/// </summary>
public List<WindowCapability> Windows { get; set; } = new();
/// <summary>
/// Check if display supports PIP/PBP windows
/// </summary>
public bool HasWindowSupport => Windows.Count > 0;
/// <summary>
/// Check if a specific VCP code is supported
/// </summary>
@@ -176,4 +186,129 @@ namespace PowerDisplay.Common.Models
return $"0x{Code:X2} ({Name}): Continuous";
}
}
/// <summary>
/// Window size (width and height)
/// </summary>
public readonly struct WindowSize
{
/// <summary>
/// Width in pixels
/// </summary>
public int Width { get; }
/// <summary>
/// Height in pixels
/// </summary>
public int Height { get; }
public WindowSize(int width, int height)
{
Width = width;
Height = height;
}
public override string ToString() => $"{Width}x{Height}";
}
/// <summary>
/// Window area coordinates (top-left and bottom-right)
/// </summary>
public readonly struct WindowArea
{
/// <summary>
/// Top-left X coordinate
/// </summary>
public int X1 { get; }
/// <summary>
/// Top-left Y coordinate
/// </summary>
public int Y1 { get; }
/// <summary>
/// Bottom-right X coordinate
/// </summary>
public int X2 { get; }
/// <summary>
/// Bottom-right Y coordinate
/// </summary>
public int Y2 { get; }
/// <summary>
/// Width of the area
/// </summary>
public int Width => X2 - X1;
/// <summary>
/// Height of the area
/// </summary>
public int Height => Y2 - Y1;
public WindowArea(int x1, int y1, int x2, int y2)
{
X1 = x1;
Y1 = y1;
X2 = x2;
Y2 = y2;
}
public override string ToString() => $"({X1},{Y1})-({X2},{Y2})";
}
/// <summary>
/// Window capability information for PIP/PBP displays
/// </summary>
public readonly struct WindowCapability
{
/// <summary>
/// Window number (1, 2, 3, etc.)
/// </summary>
public int WindowNumber { get; }
/// <summary>
/// Window type (e.g., "PIP", "PBP")
/// </summary>
public string Type { get; }
/// <summary>
/// Window area coordinates
/// </summary>
public WindowArea Area { get; }
/// <summary>
/// Maximum window size
/// </summary>
public WindowSize MaxSize { get; }
/// <summary>
/// Minimum window size
/// </summary>
public WindowSize MinSize { get; }
/// <summary>
/// Window identifier
/// </summary>
public int WindowId { get; }
public WindowCapability(
int windowNumber,
string type,
WindowArea area,
WindowSize maxSize,
WindowSize minSize,
int windowId)
{
WindowNumber = windowNumber;
Type = type ?? string.Empty;
Area = area;
MaxSize = maxSize;
MinSize = minSize;
WindowId = windowId;
}
public override string ToString() =>
$"Window{WindowNumber}: Type={Type}, Area={Area}, Max={MaxSize}, Min={MinSize}";
}
}

View File

@@ -77,9 +77,16 @@ namespace PowerDisplay.Common
public static string MonitorStateFilePath => Path.Combine(PowerDisplayFolderPath, MonitorStateFileName);
/// <summary>
/// Event name for LightSwitch theme change notifications.
/// Event name for LightSwitch light theme change notifications.
/// Signaled when LightSwitch switches to light mode.
/// </summary>
public const string LightSwitchThemeChangedEventName = "Local\\PowerToys_LightSwitch_ThemeChanged";
public const string LightSwitchLightThemeEventName = "Local\\PowerToys_LightSwitch_LightTheme";
/// <summary>
/// Event name for LightSwitch dark theme change notifications.
/// Signaled when LightSwitch switches to dark mode.
/// </summary>
public const string LightSwitchDarkThemeEventName = "Local\\PowerToys_LightSwitch_DarkTheme";
/// <summary>
/// Ensures the PowerDisplay folder exists. Creates it if necessary.

View File

@@ -8,16 +8,15 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.Win32;
namespace PowerDisplay.Common.Services
{
/// <summary>
/// Listens for LightSwitch theme change events and notifies subscribers.
/// Encapsulates all LightSwitch integration logic including:
/// - Background thread management for event listening
/// - Background thread management for event listening (light/dark theme events)
/// - LightSwitch settings file parsing
/// - System theme detection
/// Theme is determined directly from which event was signaled (not from registry).
/// </summary>
public sealed partial class LightSwitchListener : IDisposable
{
@@ -96,18 +95,29 @@ namespace PowerDisplay.Common.Services
{
Logger.LogInfo($"{LogPrefix} Event listener thread started");
using var themeChangedEvent = new EventWaitHandle(false, EventResetMode.AutoReset, PathConstants.LightSwitchThemeChangedEventName);
// Use separate events for light and dark themes to avoid race conditions
// where we might read the registry before LightSwitch has updated it
using var lightThemeEvent = new EventWaitHandle(false, EventResetMode.AutoReset, PathConstants.LightSwitchLightThemeEventName);
using var darkThemeEvent = new EventWaitHandle(false, EventResetMode.AutoReset, PathConstants.LightSwitchDarkThemeEventName);
var waitHandles = new WaitHandle[] { lightThemeEvent, darkThemeEvent };
while (!cancellationToken.IsCancellationRequested)
{
// Wait for LightSwitch to signal theme change (with timeout to allow cancellation check)
if (themeChangedEvent.WaitOne(TimeSpan.FromSeconds(1)))
{
Logger.LogInfo($"{LogPrefix} Theme change event received");
// Wait for either light or dark theme event (with timeout to allow cancellation check)
int index = WaitHandle.WaitAny(waitHandles, TimeSpan.FromSeconds(1));
// Process the theme change
_ = Task.Run(() => ProcessThemeChange(), CancellationToken.None);
if (index == WaitHandle.WaitTimeout)
{
continue;
}
// Determine theme from which event was signaled
bool isLightMode = index == 0; // 0 = lightThemeEvent, 1 = darkThemeEvent
Logger.LogInfo($"{LogPrefix} Theme event received: {(isLightMode ? "Light" : "Dark")}");
// Process the theme change with the known theme
_ = Task.Run(() => ProcessThemeChange(isLightMode), CancellationToken.None);
}
Logger.LogInfo($"{LogPrefix} Event listener thread stopping");
@@ -118,21 +128,13 @@ namespace PowerDisplay.Common.Services
}
}
private void ProcessThemeChange()
private void ProcessThemeChange(bool isLightMode)
{
try
{
Logger.LogInfo($"{LogPrefix} Processing theme change");
Logger.LogInfo($"{LogPrefix} Processing theme change to {(isLightMode ? "light" : "dark")} mode");
var result = ReadLightSwitchSettings();
if (result == null)
{
// Settings not found or integration disabled
return;
}
var (isLightMode, profileToApply) = result.Value;
var profileToApply = ReadProfileFromLightSwitchSettings(isLightMode);
if (string.IsNullOrEmpty(profileToApply) || profileToApply == "(None)")
{
@@ -152,10 +154,12 @@ namespace PowerDisplay.Common.Services
}
/// <summary>
/// Reads LightSwitch settings and determines which profile to apply
/// Reads LightSwitch settings and returns the profile name to apply for the given theme.
/// The theme is determined by which event was signaled (light or dark), not by reading the registry.
/// </summary>
/// <returns>Tuple of (isLightMode, profileName) or null if integration is disabled/unavailable</returns>
private static (bool IsLightMode, string? ProfileToApply)? ReadLightSwitchSettings()
/// <param name="isLightMode">Whether the theme is light mode (determined from the signaled event)</param>
/// <returns>The profile name to apply, or null if not configured</returns>
private static string? ReadProfileFromLightSwitchSettings(bool isLightMode)
{
try
{
@@ -186,23 +190,15 @@ namespace PowerDisplay.Common.Services
return null;
}
// Determine current theme
bool isLightMode = IsSystemInLightMode();
Logger.LogInfo($"{LogPrefix} Current system theme: {(isLightMode ? "Light" : "Dark")}");
// Get the appropriate profile name
string? profileToApply = null;
// Get the appropriate profile name based on the theme from the event
if (isLightMode)
{
profileToApply = GetProfileFromSettings(properties, "enable_light_mode_profile", "light_mode_profile");
return GetProfileFromSettings(properties, "enable_light_mode_profile", "light_mode_profile");
}
else
{
profileToApply = GetProfileFromSettings(properties, "enable_dark_mode_profile", "dark_mode_profile");
return GetProfileFromSettings(properties, "enable_dark_mode_profile", "dark_mode_profile");
}
return (isLightMode, profileToApply);
}
catch (Exception ex)
{
@@ -228,31 +224,6 @@ namespace PowerDisplay.Common.Services
return null;
}
/// <summary>
/// Check if Windows is currently in light mode
/// </summary>
public static bool IsSystemInLightMode()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (key != null)
{
var value = key.GetValue("SystemUsesLightTheme");
if (value is int intValue)
{
return intValue == 1;
}
}
}
catch (Exception ex)
{
Logger.LogError($"{LogPrefix} Failed to read system theme: {ex.Message}");
}
return false; // Default to dark mode
}
public void Dispose()
{
if (_disposed)

View File

@@ -191,8 +191,9 @@ namespace PowerDisplay.Common.Utils
}
/// <summary>
/// Parse an identifier (lowercase letters and underscores).
/// identifier ::= [a-z_]+
/// Parse an identifier (letters, digits, and underscores).
/// identifier ::= [a-zA-Z0-9_]+
/// Note: MCCS uses identifiers like window1, window2, etc.
/// </summary>
private ReadOnlySpan<char> ParseIdentifier()
{
@@ -242,8 +243,21 @@ namespace PowerDisplay.Common.Utils
break;
default:
// Store unknown segments for potential future use
Logger.LogDebug($"Unknown capabilities segment: {segment.Name}({segment.Content})");
// Check for windowN pattern (window1, window2, etc.)
if (segment.Name.Length > 6 &&
segment.Name.StartsWith("window", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(segment.Name.AsSpan(6), out int windowNum))
{
var windowParser = new WindowParser(segment.Content);
var windowCap = windowParser.Parse(windowNum);
capabilities.Windows.Add(windowCap);
}
else
{
// Store unknown segments for potential future use
Logger.LogDebug($"Unknown capabilities segment: {segment.Name}({segment.Content})");
}
break;
}
}
@@ -351,7 +365,7 @@ namespace PowerDisplay.Common.Utils
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsIdentifierChar(char c) =>
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsHexDigit(char c) =>
@@ -589,6 +603,192 @@ namespace PowerDisplay.Common.Utils
(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
/// <summary>
/// Sub-parser for window segment content.
/// Parses: type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)
/// </summary>
internal ref struct WindowParser
{
private ReadOnlySpan<char> _content;
private int _position;
public WindowParser(string content)
{
_content = content.AsSpan();
_position = 0;
}
/// <summary>
/// Parse window segment content into a WindowCapability.
/// </summary>
public WindowCapability Parse(int windowNumber)
{
string type = string.Empty;
var area = default(WindowArea);
var maxSize = default(WindowSize);
var minSize = default(WindowSize);
int windowId = 0;
// Parse sub-segments: type(...) area(...) max(...) min(...) window(...)
while (!IsAtEnd())
{
SkipWhitespace();
if (IsAtEnd())
{
break;
}
var subSegment = ParseSubSegment();
if (subSegment.HasValue)
{
switch (subSegment.Value.Name.ToLowerInvariant())
{
case "type":
type = subSegment.Value.Content.Trim();
break;
case "area":
area = ParseArea(subSegment.Value.Content);
break;
case "max":
maxSize = ParseSize(subSegment.Value.Content);
break;
case "min":
minSize = ParseSize(subSegment.Value.Content);
break;
case "window":
_ = int.TryParse(subSegment.Value.Content.Trim(), out windowId);
break;
}
}
}
return new WindowCapability(windowNumber, type, area, maxSize, minSize, windowId);
}
private (string Name, string Content)? ParseSubSegment()
{
int start = _position;
// Parse identifier
while (!IsAtEnd() && IsIdentifierChar(Peek()))
{
_position++;
}
if (_position == start)
{
// No identifier found, skip character
if (!IsAtEnd())
{
_position++;
}
return null;
}
var name = _content.Slice(start, _position - start).ToString();
SkipWhitespace();
// Expect '('
if (IsAtEnd() || Peek() != '(')
{
return null;
}
_position++; // consume '('
// Parse content with balanced parentheses
int contentStart = _position;
int depth = 1;
while (!IsAtEnd() && depth > 0)
{
char c = Peek();
if (c == '(')
{
depth++;
}
else if (c == ')')
{
depth--;
if (depth == 0)
{
break;
}
}
_position++;
}
var content = _content.Slice(contentStart, _position - contentStart).ToString();
if (!IsAtEnd() && Peek() == ')')
{
_position++; // consume ')'
}
return (name, content);
}
private static WindowArea ParseArea(string content)
{
var values = ParseIntList(content);
if (values.Length >= 4)
{
return new WindowArea(values[0], values[1], values[2], values[3]);
}
return default;
}
private static WindowSize ParseSize(string content)
{
var values = ParseIntList(content);
if (values.Length >= 2)
{
return new WindowSize(values[0], values[1]);
}
return default;
}
private static int[] ParseIntList(string content)
{
var parts = content.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var result = new List<int>(parts.Length);
foreach (var part in parts)
{
if (int.TryParse(part.Trim(), out int value))
{
result.Add(value);
}
}
return result.ToArray();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private char Peek() => IsAtEnd() ? '\0' : _content[_position];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAtEnd() => _position >= _content.Length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhitespace()
{
while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
{
_position++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsIdentifierChar(char c) =>
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
/// <summary>
/// Represents a parsed segment from the capabilities string.
/// </summary>

View File

@@ -16,12 +16,13 @@ namespace PowerDisplay.Common.Utils
/// </summary>
private static readonly Dictionary<byte, string> CodeNames = new()
{
// Control codes
// Control codes (0x00-0x0F)
{ 0x00, "Code Page" },
{ 0x01, "Degauss" },
{ 0x02, "New Control Value" },
{ 0x03, "Soft Controls" },
// Geometry codes
// Preset operations (0x04-0x0A)
{ 0x04, "Restore Factory Defaults" },
{ 0x05, "Restore Brightness and Contrast" },
{ 0x06, "Restore Factory Geometry" },
@@ -48,7 +49,7 @@ namespace PowerDisplay.Common.Utils
{ 0x1E, "Auto Setup" },
{ 0x1F, "Auto Color Setup" },
// Geometry codes
// Geometry controls (0x20-0x4C)
{ 0x20, "Horizontal Position" },
{ 0x22, "Horizontal Size" },
{ 0x24, "Horizontal Pincushion" },
@@ -57,6 +58,7 @@ namespace PowerDisplay.Common.Utils
{ 0x29, "Horizontal Convergence M/G" },
{ 0x2A, "Horizontal Linearity" },
{ 0x2C, "Horizontal Linearity Balance" },
{ 0x2E, "Gray Scale Expansion" },
{ 0x30, "Vertical Position" },
{ 0x32, "Vertical Size" },
{ 0x34, "Vertical Pincushion" },
@@ -107,12 +109,21 @@ namespace PowerDisplay.Common.Utils
{ 0x73, "LUT Size" },
{ 0x74, "Single Point LUT Operation" },
{ 0x75, "Block LUT Operation" },
{ 0x76, "Remote Procedure Call" },
{ 0x78, "Display Identification Data Operation" },
{ 0x7A, "Adjust Focal Plane" },
{ 0x7C, "Adjust Zoom" },
{ 0x7E, "Trapezoid" },
{ 0x80, "Keystone" },
{ 0x82, "Horizontal Mirror (Flip)" },
{ 0x84, "Vertical Mirror (Flip)" },
// Color calibration codes
// Image adjustment codes (0x86-0x9F)
{ 0x86, "Display Scaling" },
{ 0x87, "Sharpness" },
{ 0x88, "Velocity Scan Modulation" },
{ 0x8A, "Color Saturation" },
{ 0x8B, "TV Channel Up/Down" },
{ 0x8C, "TV Sharpness" },
{ 0x8D, "Audio Mute/Screen Blank" },
{ 0x8E, "TV Contrast" },
@@ -141,6 +152,7 @@ namespace PowerDisplay.Common.Utils
{ 0xA5, "Window Select" },
{ 0xA6, "Window Size" },
{ 0xA7, "Window Transparency" },
{ 0xA8, "Window Control" },
{ 0xAA, "Screen Orientation" },
{ 0xAC, "Horizontal Frequency" },
{ 0xAE, "Vertical Frequency" },
@@ -169,6 +181,9 @@ namespace PowerDisplay.Common.Utils
{ 0xC9, "Display Firmware Level" },
{ 0xCA, "OSD" },
{ 0xCC, "OSD Language" },
{ 0xCD, "Status Indicators" },
{ 0xCE, "Auxiliary Display Size" },
{ 0xCF, "Auxiliary Display Data" },
{ 0xD0, "Output Select" },
{ 0xD2, "Asset Tag" },
{ 0xD4, "Stereo Video Mode" },
@@ -177,11 +192,15 @@ namespace PowerDisplay.Common.Utils
{ 0xD8, "Scan Mode" },
{ 0xD9, "Image Mode" },
{ 0xDA, "On Screen Display" },
{ 0xDB, "Backlight Level: White" },
{ 0xDC, "Display Application" },
{ 0xDD, "Application Enable Key" },
{ 0xDE, "Scratch Pad" },
// Information codes
{ 0xDF, "VCP Version" },
// Manufacturer specific codes (0xE0-0xFF)
// Per MCCS 2.2a: "The 32 control codes E0h through FFh have been
// allocated to allow manufacturers to issue their own specific controls."
{ 0xE0, "Manufacturer Specific" },
{ 0xE1, "Manufacturer Specific" },
{ 0xE2, "Manufacturer Specific" },