diff --git a/.gitignore b/.gitignore index 2005deb..e6995f0 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ artifacts/ *.snupkg **/packages/* -dist/ \ No newline at end of file +dist/ + +.vscode/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a2904b0..00a8bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to ddcswitch will be documented in this file. +## [1.0.4] - 2026-01-09 + +### Added +- Enhanced visual styling across all commands with improved panels, tables, and progress bars +- Better color hierarchy and readability optimized for Windows Terminal and Command Prompt dark backgrounds +- Improved status indicators with clear symbols (โœ“/โœ—/โš ) for monitor and feature status +- Visual progress bars for percentage-based features (brightness, contrast) in `get` command +- Summary statistics in `list` command showing monitor count and DDC/CI status overview +- Better visual separation and formatting in `set`, `toggle`, and scan commands +- Documentation for `NO_COLOR` environment variable support (automatic via Spectre.Console) +- Consistent panel styling with icons for better visual hierarchy (๐Ÿ“‹โšก๐ŸŽจ๐Ÿ“ก) + +### Improved +- Error messages now include better formatting and actionable hints +- Success panels show more detailed information with clear visual structure +- Table styling enhanced for better readability on dark backgrounds +- Warning messages have improved formatting and context + ## [1.0.3] - 2026-01-08 ### Added diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 97d6e2d..0784434 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -43,7 +43,7 @@ public static void WriteMonitorDetails(Monitor monitor) { // Header with monitor identification var headerPanel = new Panel( - $"[bold white]{monitor.Name}[/]\n" + + $"[bold white]{monitor.ResolvedName}[/]\n" + $"[dim]Device:[/] [cyan]{monitor.DeviceName}[/] " + $"[dim]Primary:[/] {(monitor.IsPrimary ? "[green]Yes[/]" : "[dim]No[/]")}") { @@ -152,9 +152,9 @@ public static void WriteMonitorDetails(Monitor monitor) var chromaTable = new Table() .Border(TableBorder.Rounded) .BorderColor(Color.Grey) - .AddColumn(new TableColumn("[bold]Color[/]").Centered().Width(12)) - .AddColumn(new TableColumn("[bold]X[/]").Centered().Width(12)) - .AddColumn(new TableColumn("[bold]Y[/]").Centered().Width(12)); + .AddColumn(new TableColumn("[bold]Color[/]").LeftAligned().Width(12)) + .AddColumn(new TableColumn("[bold]X[/]").LeftAligned().Width(12)) + .AddColumn(new TableColumn("[bold]Y[/]").LeftAligned().Width(12)); chromaTable.AddRow( "[red]โ— Red[/]", diff --git a/DDCSwitch/Commands/GetCommand.cs b/DDCSwitch/Commands/GetCommand.cs index 7c6914d..c947f09 100644 --- a/DDCSwitch/Commands/GetCommand.cs +++ b/DDCSwitch/Commands/GetCommand.cs @@ -1,4 +1,4 @@ -๏ปฟusing Spectre.Console; +๏ปฟ๏ปฟusing Spectre.Console; using System.Text.Json; namespace DDCSwitch.Commands; @@ -267,39 +267,58 @@ private static void OutputFeatureValue(Monitor monitor, VcpFeature feature, uint } else { - var panel = new Panel( - $"[bold cyan]Monitor:[/] {monitor.Name}\n" + + // Header panel with monitor info + var headerPanel = new Panel( + $"[bold white]{monitor.Name}[/]\n" + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]") { - Header = new PanelHeader($"[bold green]>> Feature Value[/]", Justify.Left), + Header = new PanelHeader($"[bold cyan]๐Ÿ“Š {feature.Name}[/]", Justify.Left), Border = BoxBorder.Rounded, BorderStyle = new Style(Color.Cyan) }; - AnsiConsole.Write(panel); + AnsiConsole.Write(headerPanel); + AnsiConsole.WriteLine(); if (feature.Code == InputSource.VcpInputSource) { // Display input with name resolution var inputName = InputSource.GetName(current); - AnsiConsole.MarkupLine($" [bold yellow]{feature.Name}:[/] [cyan]{inputName}[/] [dim](0x{current:X2})[/]"); + var valuePanel = new Panel( + $"[bold cyan]{inputName}[/] [dim](0x{current:X2})[/]") + { + Header = new PanelHeader("[bold yellow]Current Input Source[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(valuePanel); } else if (feature.SupportsPercentage) { - // Display percentage for brightness/contrast + // Display percentage for brightness/contrast with progress bar uint percentage = FeatureResolver.ConvertRawToPercentage(current, max); + var progressBar = new BarChart() - .Width(40) - .Label($"[bold yellow]{feature.Name}[/]") + .Width(50) + .Label($"[bold yellow]{percentage}%[/]") .CenterLabel() - .AddItem("", percentage, Color.Green); + .AddItem("", percentage, percentage >= 75 ? Color.Green : + percentage >= 50 ? Color.Yellow : + percentage >= 25 ? Color.Orange1 : Color.Red); AnsiConsole.Write(progressBar); - AnsiConsole.MarkupLine($" [bold green]{percentage}%[/] [dim](raw: {current}/{max})[/]"); + AnsiConsole.MarkupLine($" [dim]Raw value: {current}/{max}[/]"); } else { // Display raw values for unknown VCP codes - AnsiConsole.MarkupLine($" [bold yellow]{feature.Name}:[/] [green]{current}[/] [dim](max: {max})[/]"); + var valuePanel = new Panel( + $"[bold green]{current}[/] [dim](maximum: {max})[/]") + { + Header = new PanelHeader("[bold yellow]Current Value[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(valuePanel); } } } diff --git a/DDCSwitch/Commands/InfoCommand.cs b/DDCSwitch/Commands/InfoCommand.cs index 36e431e..58399df 100644 --- a/DDCSwitch/Commands/InfoCommand.cs +++ b/DDCSwitch/Commands/InfoCommand.cs @@ -87,7 +87,7 @@ private static int DisplayMonitorInfo(Monitor monitor, bool jsonOutput) { var monitorRef = new MonitorReference( monitor.Index, - monitor.Name, + monitor.ResolvedName, monitor.DeviceName, monitor.IsPrimary); @@ -166,7 +166,7 @@ private static int DisplayMonitorInfo(Monitor monitor, bool jsonOutput) { if (jsonOutput) { - var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var monitorRef = new MonitorReference(monitor.Index, monitor.ResolvedName, monitor.DeviceName, monitor.IsPrimary); var error = new ErrorResponse(false, $"Failed to retrieve monitor information: {ex.Message}", monitorRef); Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); } @@ -204,7 +204,7 @@ private static int HandleMonitorNotFound(List monitors, string identifi else { ConsoleOutputFormatter.WriteError($"Monitor '{identifier}' not found."); - AnsiConsole.MarkupLine($"Available monitors: [cyan]{string.Join(", ", monitors.Select(m => m.Index.ToString()))}[/]"); + AnsiConsole.MarkupLine("[dim]Use [/][yellow]ddcswitch list[/][dim] to see available monitors.[/]"); } foreach (var m in monitors) diff --git a/DDCSwitch/Commands/ListCommand.cs b/DDCSwitch/Commands/ListCommand.cs index f00371c..be75f62 100644 --- a/DDCSwitch/Commands/ListCommand.cs +++ b/DDCSwitch/Commands/ListCommand.cs @@ -1,4 +1,4 @@ -๏ปฟusing Spectre.Console; +๏ปฟ๏ปฟusing Spectre.Console; using System.Text.Json; namespace DDCSwitch.Commands; @@ -118,7 +118,7 @@ private static void OutputJsonList(List monitors, bool verboseOutput) return new MonitorInfo( monitor.Index, - monitor.Name, + monitor.ResolvedName, monitor.DeviceName, monitor.IsPrimary, inputName, @@ -141,6 +141,35 @@ private static void OutputJsonList(List monitors, bool verboseOutput) private static void OutputTableList(List monitors, bool verboseOutput) { + // Summary statistics header + int ddcciCount = 0; + int primaryCount = 0; + + foreach (var monitor in monitors) + { + if (monitor.IsPrimary) primaryCount++; + try + { + if (monitor.TryGetInputSource(out _, out _)) + { + ddcciCount++; + } + } + catch { } + } + + var summaryPanel = new Panel( + $"[bold white]Total Monitors:[/] [cyan]{monitors.Count}[/] " + + $"[bold white]DDC/CI Capable:[/] [green]{ddcciCount}[/] " + + $"[bold white]Primary:[/] [yellow]{primaryCount}[/]") + { + Header = new PanelHeader("[bold cyan]๐Ÿ–ฅ๏ธ Monitor Overview[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Cyan) + }; + AnsiConsole.Write(summaryPanel); + AnsiConsole.WriteLine(); + var table = new Table() .Border(TableBorder.Rounded) .BorderColor(Color.White) @@ -165,7 +194,7 @@ private static void OutputTableList(List monitors, bool verboseOutput) foreach (var monitor in monitors) { string inputInfo = "[dim]N/A[/]"; - string status = "[green]+[/] [bold green]OK[/]"; + string status = "[green]โœ“[/] [bold green]OK[/]"; string brightnessInfo = "[dim]N/A[/]"; string contrastInfo = "[dim]N/A[/]"; @@ -178,11 +207,11 @@ private static void OutputTableList(List monitors, bool verboseOutput) } else { - status = "[yellow]~[/] [bold yellow]No DDC/CI[/]"; + status = "[yellow]โš [/] [bold yellow]No DDC/CI[/]"; } // Get brightness and contrast if verbose mode is enabled and monitor supports DDC/CI - if (verboseOutput && status == "[green]+[/] [bold green]OK[/]") + if (verboseOutput && status == "[green]โœ“[/] [bold green]OK[/]") { // Try to get brightness (VCP 0x10) if (monitor.TryGetVcpFeature(VcpFeature.Brightness.Code, out uint brightnessCurrent, out uint brightnessMax)) @@ -214,7 +243,7 @@ private static void OutputTableList(List monitors, bool verboseOutput) } catch { - status = "[red]X[/] [bold red]Error[/]"; + status = "[red]โœ—[/] [bold red]Error[/]"; if (verboseOutput) { brightnessInfo = "[dim]N/A[/]"; @@ -224,8 +253,8 @@ private static void OutputTableList(List monitors, bool verboseOutput) var row = new List { - monitor.IsPrimary ? $"[bold cyan]{monitor.Index}[/] [yellow]*[/]" : $"[cyan]{monitor.Index}[/]", - monitor.Name, + monitor.IsPrimary ? $"[bold cyan]{monitor.Index}[/] [yellow]โ—[/]" : $"[cyan]{monitor.Index}[/]", + monitor.ResolvedName, $"[dim]{monitor.DeviceName}[/]", inputInfo }; diff --git a/DDCSwitch/Commands/SetCommand.cs b/DDCSwitch/Commands/SetCommand.cs index ce4b6c2..c6b73d8 100644 --- a/DDCSwitch/Commands/SetCommand.cs +++ b/DDCSwitch/Commands/SetCommand.cs @@ -1,4 +1,4 @@ -๏ปฟusing Spectre.Console; +๏ปฟ๏ปฟusing Spectre.Console; using System.Text.Json; namespace DDCSwitch.Commands; @@ -369,28 +369,33 @@ private static void OutputSuccess(Monitor monitor, VcpFeature feature, uint setV else { string displayValue; + string featureIcon; + if (feature.Code == InputSource.VcpInputSource) { // Display input with name resolution - displayValue = $"[cyan]{InputSource.GetName(setValue)}[/]"; + displayValue = $"[bold cyan]{InputSource.GetName(setValue)}[/] [dim](0x{setValue:X2})[/]"; + featureIcon = "๐Ÿ“บ"; } else if (percentageValue.HasValue) { // Display percentage for brightness/contrast - displayValue = $"[green]{percentageValue}%[/]"; + displayValue = $"[bold green]{percentageValue}%[/] [dim](raw: {setValue})[/]"; + featureIcon = feature.Code == VcpFeature.Brightness.Code ? "โ˜€๏ธ" : "๐ŸŽจ"; } else { // Display raw value for unknown VCP codes - displayValue = $"[green]{setValue}[/]"; + displayValue = $"[bold green]{setValue}[/]"; + featureIcon = "โš™๏ธ"; } var successPanel = new Panel( - $"[bold cyan]Monitor:[/] {monitor.Name}\n" + - $"[bold yellow]Feature:[/] {feature.Name}\n" + - $"[bold green]New Value:[/] {displayValue}") + $"[bold white]{monitor.Name}[/]\n" + + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]\n\n" + + $"[bold yellow]{feature.Name}:[/] {displayValue}") { - Header = new PanelHeader("[bold green]>> Successfully Applied[/]", Justify.Left), + Header = new PanelHeader($"[bold green]โœ“ {featureIcon} Successfully Applied[/]", Justify.Left), Border = BoxBorder.Rounded, BorderStyle = new Style(Color.Green) }; diff --git a/DDCSwitch/Commands/ToggleCommand.cs b/DDCSwitch/Commands/ToggleCommand.cs index e2c99aa..0e9cf77 100644 --- a/DDCSwitch/Commands/ToggleCommand.cs +++ b/DDCSwitch/Commands/ToggleCommand.cs @@ -379,11 +379,11 @@ private static void OutputToggleSuccess(Monitor monitor, uint currentInput, uint else { var successPanel = new Panel( - $"[bold cyan]Monitor:[/] {monitor.Name}\n" + - $"[bold yellow]From:[/] {fromInputName}\n" + - $"[bold green]To:[/] {toInputName}") + $"[bold white]{monitor.Name}[/]\n" + + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]\n\n" + + $"[cyan]{fromInputName}[/] [dim](0x{currentInput:X2})[/] [bold yellow]โ†’[/] [bold cyan]{toInputName}[/] [dim](0x{targetInput:X2})[/]") { - Header = new PanelHeader("[bold green]>> Input Toggled Successfully[/]", Justify.Left), + Header = new PanelHeader("[bold green]โœ“ ๐Ÿ”„ Input Toggled Successfully[/]", Justify.Left), Border = BoxBorder.Rounded, BorderStyle = new Style(Color.Green) }; @@ -392,7 +392,14 @@ private static void OutputToggleSuccess(Monitor monitor, uint currentInput, uint if (hasWarning && warningMessage != null) { - AnsiConsole.MarkupLine($"[yellow]Warning:[/] {warningMessage}"); + AnsiConsole.WriteLine(); + var warningPanel = new Panel(warningMessage) + { + Header = new PanelHeader("[bold yellow]โš  Warning[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(warningPanel); } } } diff --git a/DDCSwitch/Commands/VcpScanCommand.cs b/DDCSwitch/Commands/VcpScanCommand.cs index b2466ae..863de53 100644 --- a/DDCSwitch/Commands/VcpScanCommand.cs +++ b/DDCSwitch/Commands/VcpScanCommand.cs @@ -199,7 +199,8 @@ private static void OutputTableScanAll(List monitors) Style = new Style(Color.Cyan) }; AnsiConsole.Write(rule); - + AnsiConsole.WriteLine(); + Dictionary features = null!; AnsiConsole.Status() .Start($"Scanning VCP features for {monitor.Name}...", ctx => @@ -221,7 +222,15 @@ private static void OutputTableScanAll(List monitors) continue; } - AnsiConsole.MarkupLine($"[bold green]>> Found {supportedFeatures.Count} supported features[/]\n"); + // Show feature count summary + int readWriteCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.ReadWrite); + int readOnlyCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.ReadOnly); + int writeOnlyCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.WriteOnly); + + AnsiConsole.MarkupLine( + $"[bold green]โœ“ Found {supportedFeatures.Count} supported features[/] " + + $"[dim]([green]{readWriteCount}[/] R/W, [yellow]{readOnlyCount}[/] R, [red]{writeOnlyCount}[/] W)[/]\n"); + OutputFeatureTable(supportedFeatures); AnsiConsole.WriteLine(); } @@ -241,16 +250,23 @@ private static void OutputJsonScanSingle(MonitorReference monitorRef, List supportedFeatures) { - var panel = new Panel( - $"[bold cyan]Monitor:[/] {monitor.Name}\n" + - $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]\n" + - $"[bold yellow]Supported Features:[/] [green]{supportedFeatures.Count}[/]") + // Summary panel with feature count breakdown + int readWriteCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.ReadWrite); + int readOnlyCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.ReadOnly); + int writeOnlyCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.WriteOnly); + + var summaryPanel = new Panel( + $"[bold white]{monitor.Name}[/]\n" + + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]\n\n" + + $"[bold yellow]Total Features:[/] [green]{supportedFeatures.Count}[/] " + + $"[dim]([green]{readWriteCount}[/] R/W, [yellow]{readOnlyCount}[/] R, [red]{writeOnlyCount}[/] W)[/]") { - Header = new PanelHeader($"[bold cyan]>> VCP Feature Scan Results[/]", Justify.Left), + Header = new PanelHeader($"[bold cyan]๐Ÿ” VCP Feature Scan Results[/]", Justify.Left), Border = BoxBorder.Rounded, - BorderStyle = new Style(Color.White) + BorderStyle = new Style(Color.Cyan) }; - AnsiConsole.Write(panel); + AnsiConsole.Write(summaryPanel); + AnsiConsole.WriteLine(); if (supportedFeatures.Count == 0) { @@ -266,7 +282,7 @@ private static void OutputFeatureTable(List supportedFeatures) { var table = new Table() .Border(TableBorder.Rounded) - .BorderColor(Color.White) + .BorderColor(Color.Grey) .AddColumn(new TableColumn("[bold yellow]VCP Code[/]").Centered()) .AddColumn(new TableColumn("[bold yellow]Feature Name[/]").LeftAligned()) .AddColumn(new TableColumn("[bold yellow]Access[/]").Centered()) @@ -278,10 +294,10 @@ private static void OutputFeatureTable(List supportedFeatures) string vcpCode = $"[cyan]0x{feature.Code:X2}[/]"; string accessType = feature.Type switch { - VcpFeatureType.ReadOnly => "[yellow]Read Only[/]", - VcpFeatureType.WriteOnly => "[red]Write Only[/]", - VcpFeatureType.ReadWrite => "[green]Read+Write[/]", - _ => "[dim]? ?[/]" + VcpFeatureType.ReadOnly => "[yellow]โœ“[/] [dim]Read[/]", + VcpFeatureType.WriteOnly => "[red]โœŽ[/] [dim]Write[/]", + VcpFeatureType.ReadWrite => "[green]โœ“โœŽ[/] [dim]R/W[/]", + _ => "[dim]?[/]" }; string currentValue = $"[green]{feature.CurrentValue}[/]"; @@ -294,6 +310,19 @@ private static void OutputFeatureTable(List supportedFeatures) { name = $"[dim]{feature.Name}[/]"; } + else + { + // Add icon for common features + name = feature.Code switch + { + 0x10 => $"โ˜€๏ธ {feature.Name}", + 0x12 => $"๐ŸŽจ {feature.Name}", + 0x60 => $"๐Ÿ“บ {feature.Name}", + 0x62 => $"๐Ÿ”Š {feature.Name}", + 0x8D => $"๐Ÿ”‡ {feature.Name}", + _ => feature.Name + }; + } table.AddRow(vcpCode, name, accessType, currentValue, maxValue); } diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Core/Monitor.cs similarity index 59% rename from DDCSwitch/Monitor.cs rename to DDCSwitch/Core/Monitor.cs index 2061fa6..2c7237a 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Core/Monitor.cs @@ -13,7 +13,17 @@ public class Monitor(int index, string name, string deviceName, bool isPrimary, public string DeviceName { get; } = deviceName; public bool IsPrimary { get; } = isPrimary; - // EDID properties + // Enhanced name resolution with DDC/CI priority + public string ResolvedName { get; private set; } = name; // Initialize with Windows name + public bool NameFromDdcCi { get; private set; } + public bool NameFromEdid { get; private set; } + + // EDID properties (enhanced with registry conflict resolution) + public ParsedEdidInfo? ParsedEdid { get; private set; } + public List? AlternativeRegistryEntries { get; private set; } + public bool HasRegistryConflicts => AlternativeRegistryEntries?.Count > 1; + + // Legacy EDID properties (for backward compatibility) public string? ManufacturerId { get; private set; } public string? ManufacturerName { get; private set; } public string? ModelName { get; private set; } @@ -26,37 +36,154 @@ public class Monitor(int index, string name, string deviceName, bool isPrimary, public SupportedFeatures? SupportedFeatures { get; private set; } public ChromaticityCoordinates? Chromaticity { get; private set; } + // DDC/CI identification properties + public MonitorIdentityInfo? DdcCiIdentity { get; private set; } + public VcpCapabilityInfo? VcpCapabilities { get; private set; } + private IntPtr Handle { get; } = handle; private bool _disposed; /// - /// Loads EDID data from registry and populates EDID properties. + /// Loads EDID data from registry with enhanced conflict resolution and populates EDID properties. /// public void LoadEdidData() { try { - var edid = NativeMethods.GetEdidFromRegistry(DeviceName); - if (edid == null || edid.Length < 128) return; + // Use the enhanced registry method with monitor description matching first + var edidData = NativeMethods.GetEdidFromRegistryEnhanced(DeviceName, Handle, Name); + if (edidData != null && edidData.Length >= 128) + { + ParsedEdid = EdidParser.ParseFromBytes(edidData, true); + } + else + { + // Fallback to active registry parsing + ParsedEdid = EdidParser.ParseFromActiveRegistry(DeviceName, Handle); + } + + // Final fallback to legacy method + if (ParsedEdid == null) + { + ParsedEdid = EdidParser.ParseFromRegistry(DeviceName); + } + + // Load alternative registry entries for conflict detection + if (ParsedEdid != null) + { + string? hardwareId = ExtractHardwareIdFromDeviceName(DeviceName); + if (hardwareId != null) + { + AlternativeRegistryEntries = EdidParser.FindAllRegistryEntries(hardwareId); + } + } - ManufacturerId = EdidParser.ParseManufacturerId(edid); - if (ManufacturerId != null) + // Populate legacy properties for backward compatibility + if (ParsedEdid != null) { - ManufacturerName = EdidParser.GetManufacturerName(ManufacturerId); + ManufacturerId = ParsedEdid.ManufacturerCode; + ManufacturerName = ParsedEdid.ManufacturerName; + ProductCode = ParsedEdid.ProductCode; + ManufactureYear = ParsedEdid.ManufactureYear; + ManufactureWeek = ParsedEdid.ManufactureWeek; + + // Parse additional legacy properties from raw EDID data + var edid = ParsedEdid.RawData; + ModelName = EdidParser.ParseModelName(edid); + SerialNumber = EdidParser.ParseSerialNumber(edid); + EdidVersion = EdidParser.ParseEdidVersion(edid); + VideoInputDefinition = EdidParser.ParseVideoInputDefinition(edid); + SupportedFeatures = EdidParser.ParseSupportedFeatures(edid); + Chromaticity = EdidParser.ParseChromaticity(edid); } - ModelName = EdidParser.ParseModelName(edid); - SerialNumber = EdidParser.ParseSerialNumber(edid); - ProductCode = EdidParser.ParseProductCode(edid); - ManufactureYear = EdidParser.ParseManufactureYear(edid); - ManufactureWeek = EdidParser.ParseManufactureWeek(edid); - EdidVersion = EdidParser.ParseEdidVersion(edid); - VideoInputDefinition = EdidParser.ParseVideoInputDefinition(edid); - SupportedFeatures = EdidParser.ParseSupportedFeatures(edid); - Chromaticity = EdidParser.ParseChromaticity(edid); + + // Resolve monitor name with DDC/CI priority + ResolveMonitorName(); } catch { // Graceful degradation - EDID properties remain null + ResolvedName = Name; // Fallback to Windows name + } + } + + /// + /// Resolves the monitor name using DDC/CI first, then EDID, then Windows fallback. + /// + private void ResolveMonitorName() + { + try + { + // Load DDC/CI identity if not already loaded + if (DdcCiIdentity == null) + { + LoadDdcCiIdentity(); + } + + // Resolve name with priority: DDC/CI > EDID > Windows + ResolvedName = MonitorNameResolver.ResolveMonitorName(this, DdcCiIdentity, ParsedEdid); + + // Set flags to indicate name source + NameFromDdcCi = MonitorNameResolver.HasNameFromDdcCi(DdcCiIdentity); + NameFromEdid = !NameFromDdcCi && MonitorNameResolver.HasNameFromEdid(ParsedEdid); + } + catch + { + // Fallback to Windows name + ResolvedName = MonitorNameResolver.GetFallbackName(this); + NameFromDdcCi = false; + NameFromEdid = false; + } + } + + /// + /// Extracts hardware ID from Windows device name (simplified implementation). + /// + /// Device name (e.g., \\.\DISPLAY1) + /// Hardware ID or null if cannot be extracted + private static string? ExtractHardwareIdFromDeviceName(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return null; + + // Extract display number from device name (e.g., \\.\DISPLAY1 -> 1) + string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); + if (int.TryParse(displayNum, out int displayIndex)) + { + // Use display index as a simple hardware ID approximation + return $"DISPLAY{displayIndex}"; + } + + return null; + } + + /// + /// Loads DDC/CI identity information and populates DDC/CI properties. + /// + public void LoadDdcCiIdentity() + { + try + { + DdcCiIdentity = DdcCiMonitorIdentifier.GetIdentityViaDdcCi(this); + } + catch + { + // Graceful degradation - DDC/CI identity remains null + } + } + + /// + /// Loads VCP capability information and populates VCP properties. + /// + public void LoadVcpCapabilities() + { + try + { + VcpCapabilities = VcpAnalyzer.AnalyzeCapabilities(this); + } + catch + { + // Graceful degradation - VCP capabilities remain null } } diff --git a/DDCSwitch/MonitorController.cs b/DDCSwitch/Core/MonitorController.cs similarity index 100% rename from DDCSwitch/MonitorController.cs rename to DDCSwitch/Core/MonitorController.cs diff --git a/DDCSwitch/Core/NativeMethods.cs b/DDCSwitch/Core/NativeMethods.cs new file mode 100644 index 0000000..d287ded --- /dev/null +++ b/DDCSwitch/Core/NativeMethods.cs @@ -0,0 +1,540 @@ +๏ปฟusing System; +using System.Runtime.InteropServices; +using Microsoft.Win32; + +namespace DDCSwitch; + +internal static class NativeMethods +{ + // Monitor enumeration + [DllImport("user32.dll")] + public static extern bool EnumDisplayMonitors( + IntPtr hdc, + IntPtr lprcClip, + MonitorEnumProc lpfnEnum, + IntPtr dwData); + + public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + // Physical monitor structures + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct PHYSICAL_MONITOR + { + public IntPtr hPhysicalMonitor; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szPhysicalMonitorDescription; + } + + // DDC/CI functions from dxva2.dll + [DllImport("dxva2.dll", SetLastError = true)] + public static extern bool GetNumberOfPhysicalMonitorsFromHMONITOR( + IntPtr hMonitor, + out uint pdwNumberOfPhysicalMonitors); + + [DllImport("dxva2.dll", SetLastError = true)] + public static extern bool GetPhysicalMonitorsFromHMONITOR( + IntPtr hMonitor, + uint dwPhysicalMonitorArraySize, + [Out] PHYSICAL_MONITOR[] pPhysicalMonitorArray); + + [DllImport("dxva2.dll", SetLastError = true)] + public static extern bool GetVCPFeatureAndVCPFeatureReply( + IntPtr hMonitor, + byte bVCPCode, + out uint pvct, + out uint pdwCurrentValue, + out uint pdwMaximumValue); + + [DllImport("dxva2.dll", SetLastError = true)] + public static extern bool SetVCPFeature( + IntPtr hMonitor, + byte bVCPCode, + uint dwNewValue); + + [DllImport("dxva2.dll", SetLastError = true)] + public static extern bool DestroyPhysicalMonitor(IntPtr hMonitor); + + [DllImport("dxva2.dll", SetLastError = true)] + public static extern bool DestroyPhysicalMonitors( + uint dwPhysicalMonitorArraySize, + PHYSICAL_MONITOR[] pPhysicalMonitorArray); + + // Additional Windows API for getting EDID directly from monitor handle + [DllImport("gdi32.dll", SetLastError = true)] + public static extern IntPtr CreateDC( + string? lpszDriver, + string lpszDevice, + string? lpszOutput, + IntPtr lpInitData); + + [DllImport("gdi32.dll", SetLastError = true)] + public static extern bool DeleteDC(IntPtr hdc); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr MonitorFromPoint( + POINT pt, + uint dwFlags); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr MonitorFromWindow( + IntPtr hwnd, + uint dwFlags); + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + } + + public const uint MONITOR_DEFAULTTONULL = 0x00000000; + public const uint MONITOR_DEFAULTTOPRIMARY = 0x00000001; + public const uint MONITOR_DEFAULTTONEAREST = 0x00000002; + + // Monitor info structures + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct MONITORINFOEX + { + public uint cbSize; + public RECT rcMonitor; + public RECT rcWork; + public uint dwFlags; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string szDevice; + } + + public const uint MONITORINFOF_PRIMARY = 0x00000001; + + /// + /// Attempts to retrieve EDID data for a specific physical monitor using enhanced registry mapping. + /// This method properly maps physical monitors to their corresponding registry entries by matching monitor descriptions. + /// + /// Device name from MONITORINFOEX (e.g., \\.\DISPLAY1) + /// Physical monitor handle for precise mapping + /// Physical monitor description from Windows + /// EDID byte array or null if not found + public static byte[]? GetEdidFromRegistryEnhanced(string deviceName, IntPtr physicalMonitorHandle, string? physicalMonitorDescription = null) + { + try + { + // Get all available EDID entries from registry + var allEdidEntries = GetAllRegistryEdidEntries(); + if (allEdidEntries.Count == 0) + return null; + + // Try to match by monitor description if available + if (!string.IsNullOrEmpty(physicalMonitorDescription)) + { + var matchingEntry = FindEdidByDescription(allEdidEntries, physicalMonitorDescription); + if (matchingEntry.HasValue) + { + return matchingEntry.Value.edidData; + } + } + + // For "Generic PnP Monitor", use process of elimination + if (physicalMonitorDescription == "Generic PnP Monitor") + { + var remainingEntry = FindRemainingEdidEntry(allEdidEntries); + if (remainingEntry.HasValue) + { + return remainingEntry.Value.edidData; + } + } + + // Fallback: Use device name based mapping with better heuristics + return GetEdidByDeviceNameHeuristic(deviceName, allEdidEntries); + } + catch + { + // Fallback to original method if enhanced method fails + return GetEdidFromRegistry(deviceName); + } + } + + /// + /// Finds the remaining EDID entry by process of elimination. + /// This is used for "Generic PnP Monitor" which doesn't have a descriptive name. + /// + /// Available EDID entries + /// The remaining EDID entry that hasn't been matched yet + private static (byte[] edidData, DateTime lastWriteTime, string registryPath)? FindRemainingEdidEntry( + List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> edidEntries) + { + // Get all currently active monitors to see which ones have been matched + var activeMonitors = GetCurrentPhysicalMonitors(); + + // Filter out entries that would be matched by other monitors + var unmatchedEntries = new List<(byte[] edidData, DateTime lastWriteTime, string registryPath)>(); + + foreach (var entry in edidEntries) + { + var modelName = EdidParser.ParseModelName(entry.edidData); + var manufacturerName = EdidParser.GetManufacturerName(EdidParser.ParseManufacturerId(entry.edidData)); + + // Skip entries that would be matched by descriptive monitor names + bool wouldBeMatched = false; + + foreach (var (handle, description) in activeMonitors) + { + if (description != "Generic PnP Monitor" && !string.IsNullOrEmpty(modelName)) + { + if (description.Contains(modelName, StringComparison.OrdinalIgnoreCase) || + (!string.IsNullOrEmpty(manufacturerName) && description.Contains(manufacturerName, StringComparison.OrdinalIgnoreCase))) + { + wouldBeMatched = true; + break; + } + } + } + + if (!wouldBeMatched) + { + unmatchedEntries.Add(entry); + } + } + + // Return the most recent unmatched entry + return unmatchedEntries + .OrderByDescending(e => e.lastWriteTime) + .FirstOrDefault(); + } + + /// + /// Finds EDID entry that matches a physical monitor description. + /// + /// Available EDID entries + /// Physical monitor description from Windows + /// Matching EDID entry or null + private static (byte[] edidData, DateTime lastWriteTime, string registryPath)? FindEdidByDescription( + List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> edidEntries, + string description) + { + // Try exact model name matches first + foreach (var entry in edidEntries) + { + var modelName = EdidParser.ParseModelName(entry.edidData); + var manufacturerId = EdidParser.ParseManufacturerId(entry.edidData); + var manufacturerName = EdidParser.GetManufacturerName(manufacturerId); + + if (!string.IsNullOrEmpty(modelName)) + { + // Check if description contains the model name + if (description.Contains(modelName, StringComparison.OrdinalIgnoreCase)) + { + return entry; + } + } + } + + // Try manufacturer name matches + foreach (var entry in edidEntries) + { + var manufacturerId = EdidParser.ParseManufacturerId(entry.edidData); + var manufacturerName = EdidParser.GetManufacturerName(manufacturerId); + + if (!string.IsNullOrEmpty(manufacturerName)) + { + // Check if description contains manufacturer name + if (description.Contains(manufacturerName, StringComparison.OrdinalIgnoreCase)) + { + return entry; + } + } + } + + // Try manufacturer ID matches + foreach (var entry in edidEntries) + { + var manufacturerId = EdidParser.ParseManufacturerId(entry.edidData); + if (!string.IsNullOrEmpty(manufacturerId)) + { + // Check if description contains manufacturer ID + if (description.Contains(manufacturerId, StringComparison.OrdinalIgnoreCase)) + { + return entry; + } + } + } + + return null; + } + + /// + /// Gets EDID data using device name with improved heuristics. + /// + /// Device name + /// Available EDID entries + /// EDID data or null + private static byte[]? GetEdidByDeviceNameHeuristic(string deviceName, + List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> edidEntries) + { + // Extract display index from device name + string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); + if (!int.TryParse(displayNum, out int displayIndex)) + return null; + + // Get currently active monitors to determine proper mapping + var activeMonitors = GetCurrentPhysicalMonitors(); + int monitorIndex = displayIndex - 1; // Convert to 0-based + + if (monitorIndex < 0 || monitorIndex >= activeMonitors.Count) + return null; + + // Sort EDID entries by recency and uniqueness + var uniqueEdids = FilterUniqueEdidEntries(edidEntries); + var sortedEdids = uniqueEdids + .OrderByDescending(e => e.lastWriteTime) + .Take(activeMonitors.Count) + .ToList(); + + // Map by index among the most recent unique entries + if (monitorIndex < sortedEdids.Count) + { + return sortedEdids[monitorIndex].edidData; + } + + return null; + } + + /// + /// Filters EDID entries to remove duplicates based on manufacturer and product signatures. + /// + /// All EDID entries + /// Filtered unique EDID entries + private static List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> FilterUniqueEdidEntries( + List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> edidEntries) + { + var uniqueEntries = new List<(byte[] edidData, DateTime lastWriteTime, string registryPath)>(); + var seenSignatures = new HashSet(); + + foreach (var entry in edidEntries.OrderByDescending(e => e.lastWriteTime)) + { + var manufacturerId = EdidParser.ParseManufacturerId(entry.edidData); + var productCode = EdidParser.ParseProductCode(entry.edidData); + var serialNumber = EdidParser.ParseNumericSerialNumber(entry.edidData); + + // Create a unique signature including serial number for better uniqueness + var signature = $"{manufacturerId}_{productCode:X4}_{serialNumber}"; + + if (!seenSignatures.Contains(signature)) + { + seenSignatures.Add(signature); + uniqueEntries.Add(entry); + } + } + + return uniqueEntries; + } + + /// + /// Gets all EDID entries from the registry with metadata. + /// + /// List of EDID entries with timestamps + private static List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> GetAllRegistryEdidEntries() + { + var entries = new List<(byte[] edidData, DateTime lastWriteTime, string registryPath)>(); + + try + { + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return entries; + + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); + if (deviceParams == null) continue; + + var edidData = deviceParams.GetValue("EDID") as byte[]; + if (edidData == null || edidData.Length < 128) + continue; + + if (!EdidParser.ValidateHeader(edidData)) + continue; + + // Use a heuristic for last write time based on registry structure + var lastWriteTime = EstimateRegistryEntryAge(instanceSubKey); + var registryPath = $@"{displayKey}\{mfgKey}\{instanceKey}"; + + entries.Add((edidData, lastWriteTime, registryPath)); + } + } + } + catch + { + // Return partial results + } + + return entries; + } + + /// + /// Gets information about currently active physical monitors. + /// + /// List of active monitor information + private static List<(IntPtr handle, string description)> GetCurrentPhysicalMonitors() + { + var monitors = new List<(IntPtr handle, string description)>(); + + try + { + NativeMethods.MonitorEnumProc callback = (IntPtr hMonitor, IntPtr hdcMonitor, ref NativeMethods.RECT lprcMonitor, IntPtr dwData) => + { + if (NativeMethods.GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, out uint count) && count > 0) + { + var physicalMonitors = new NativeMethods.PHYSICAL_MONITOR[count]; + if (NativeMethods.GetPhysicalMonitorsFromHMONITOR(hMonitor, count, physicalMonitors)) + { + foreach (var pm in physicalMonitors) + { + monitors.Add((pm.hPhysicalMonitor, pm.szPhysicalMonitorDescription)); + } + } + } + return true; + }; + + NativeMethods.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, callback, IntPtr.Zero); + } + catch + { + // Return partial results + } + + return monitors; + } + + /// + /// Estimates the age of a registry entry using heuristics. + /// + /// Registry key to analyze + /// Estimated last modification time + private static DateTime EstimateRegistryEntryAge(RegistryKey key) + { + try + { + // Heuristic: entries with more subkeys/values are likely more recent + var subKeyCount = key.GetSubKeyNames().Length; + var valueCount = key.GetValueNames().Length; + + if (subKeyCount > 5 || valueCount > 10) + { + return DateTime.Now.AddDays(-1); // Very recent + } + else if (subKeyCount > 0 || valueCount > 5) + { + return DateTime.Now.AddDays(-7); // Recent + } + else + { + return DateTime.Now.AddDays(-30); // Older + } + } + catch + { + return DateTime.Now.AddDays(-30); + } + } + public static byte[]? GetEdidFromRegistry(string deviceName) + { + try + { + // Enumerate all DISPLAY devices in registry + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return null; + + // Collect all EDIDs from active monitors + var edidList = new List(); + + // Try each monitor manufacturer key + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + // Try each instance under this manufacturer + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + // Check if this is an active device + using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); + if (deviceParams == null) continue; + + // Read EDID data + var edidData = deviceParams.GetValue("EDID") as byte[]; + if (edidData != null && edidData.Length >= 128) + { + // Validate EDID header + if (EdidParser.ValidateHeader(edidData)) + { + edidList.Add(edidData); + } + } + } + } + + // Extract display index from device name (e.g., \\.\DISPLAY1 -> 0-based index 0) + string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); + if (!int.TryParse(displayNum, out int displayIndex)) + return null; + + // Convert 1-based display number to 0-based index + int listIndex = displayIndex - 1; + + // Return EDID at the corresponding index if available + if (listIndex >= 0 && listIndex < edidList.Count) + { + return edidList[listIndex]; + } + + // No reliable EDID mapping found for this display index + return null; + } + catch (System.Security.SecurityException) + { + // No access to registry + return null; + } + catch (System.UnauthorizedAccessException) + { + // Access denied + return null; + } + catch (System.IO.IOException) + { + // Registry I/O error + return null; + } + catch (Exception) + { + // Unexpected error + return null; + } + } +} diff --git a/DDCSwitch/DDCSwitch.csproj b/DDCSwitch/DDCSwitch.csproj index 835173e..e4607e7 100644 --- a/DDCSwitch/DDCSwitch.csproj +++ b/DDCSwitch/DDCSwitch.csproj @@ -47,7 +47,7 @@ - + True True VcpFeatureData.json diff --git a/DDCSwitch/Hardware/EdidParser.cs b/DDCSwitch/Hardware/EdidParser.cs new file mode 100644 index 0000000..72b2949 --- /dev/null +++ b/DDCSwitch/Hardware/EdidParser.cs @@ -0,0 +1,990 @@ +using System.Text; +using Microsoft.Win32; + +namespace DDCSwitch; + +/// +/// Represents EDID version information. +/// +/// Major version number +/// Minor version number +public record EdidVersion(byte Major, byte Minor) +{ + public override string ToString() => $"{Major}.{Minor}"; +} + +/// +/// Represents video input definition from EDID. +/// +/// True if digital input, false if analog +/// Raw byte value from EDID +public record VideoInputDefinition(bool IsDigital, byte RawValue) +{ + public override string ToString() => IsDigital ? "Digital" : "Analog"; +} + +/// +/// Represents supported display features from EDID. +/// +public record SupportedFeatures( + bool DpmsStandby, + bool DpmsSuspend, + bool DpmsActiveOff, + byte DisplayType, + bool DefaultColorSpace, + bool PreferredTimingMode, + bool ContinuousFrequency, + byte RawValue) +{ + public string DisplayTypeDescription => DisplayType switch + { + 0 => "Monochrome or Grayscale", + 1 => "RGB Color", + 2 => "Non-RGB Color", + 3 => "Undefined", + _ => "Unknown" + }; +} + +/// +/// Represents chromaticity coordinates for a color point. +/// +/// X coordinate (0.0 to 1.0) +/// Y coordinate (0.0 to 1.0) +public record ColorPoint(double X, double Y) +{ + public override string ToString() => $"x={X:F4}, y={Y:F4}"; +} + +/// +/// Represents complete chromaticity information from EDID. +/// +public record ChromaticityCoordinates( + ColorPoint Red, + ColorPoint Green, + ColorPoint Blue, + ColorPoint White); + +/// +/// Represents a registry EDID entry with metadata. +/// +/// Full registry path to the EDID entry +/// Raw EDID byte data +/// Last modification time of the registry entry +/// Whether this entry corresponds to an active monitor +public record RegistryEdidEntry( + string RegistryPath, + byte[] EdidData, + DateTime LastWriteTime, + bool IsCurrentlyActive +); + +/// +/// Represents complete EDID information with metadata from enhanced parsing. +/// +/// Full manufacturer name +/// 3-letter manufacturer code +/// Product code from EDID +/// Serial number from EDID +/// Week of manufacture (1-53) +/// Year of manufacture +/// EDID version number +/// EDID revision number +/// Video input definition string +/// Color space information +/// Raw EDID byte data +/// Whether this EDID came from an active registry entry +public record ParsedEdidInfo( + string ManufacturerName, + string ManufacturerCode, + ushort ProductCode, + uint SerialNumber, + int ManufactureWeek, + int ManufactureYear, + byte EdidVersion, + byte EdidRevision, + string VideoInputDefinition, + EdidColorInfo ColorInfo, + byte[] RawData, + bool IsFromActiveEntry +); + +/// +/// Represents color space information from EDID. +/// +/// White point X coordinate +/// White point Y coordinate +/// Red X coordinate +/// Red Y coordinate +/// Green X coordinate +/// Green Y coordinate +/// Blue X coordinate +/// Blue Y coordinate +public record EdidColorInfo( + float WhitePointX, + float WhitePointY, + float RedX, + float RedY, + float GreenX, + float GreenY, + float BlueX, + float BlueY +); + +/// +/// Parses EDID (Extended Display Identification Data) blocks to extract monitor information. +/// Enhanced with registry conflict resolution and active entry detection. +/// +public static class EdidParser +{ + /// + /// Parses EDID information from registry with enhanced conflict resolution. + /// + /// Device name from MONITORINFOEX (e.g., \\.\DISPLAY1) + /// Complete EDID information or null if not found + public static ParsedEdidInfo? ParseFromRegistry(string deviceName) + { + var edidData = NativeMethods.GetEdidFromRegistry(deviceName); + if (edidData == null || edidData.Length < 128) + return null; + + return ParseFromBytes(edidData, false); + } + + /// + /// Parses EDID information from active registry entry using physical monitor handle. + /// + /// Device name from MONITORINFOEX + /// Physical monitor handle for cross-referencing + /// Complete EDID information from active entry or null if not found + public static ParsedEdidInfo? ParseFromActiveRegistry(string deviceName, IntPtr physicalMonitorHandle) + { + try + { + // Extract hardware ID from device name for registry lookup + string? hardwareId = ExtractHardwareIdFromDeviceName(deviceName); + if (hardwareId == null) + return null; + + // Find the active registry entry for this hardware ID + var activeEntry = FindActiveRegistryEntry(hardwareId, physicalMonitorHandle); + + return activeEntry == null ? null : ParseFromBytes(activeEntry.EdidData, true); + } + catch + { + return null; + } + } + + /// + /// Parses EDID information from raw byte data. + /// + /// Raw EDID byte array + /// Whether this data came from an active registry entry + /// Complete EDID information or null if invalid + public static ParsedEdidInfo? ParseFromBytes(byte[] edidData, bool isFromActiveEntry = false) + { + if (edidData == null || edidData.Length < 128 || !ValidateHeader(edidData)) + return null; + + try + { + var manufacturerCode = ParseManufacturerId(edidData) ?? "UNK"; + var manufacturerName = GetManufacturerName(manufacturerCode); + var productCode = ParseProductCode(edidData) ?? 0; + var serialNumber = ParseNumericSerialNumber(edidData) ?? 0; + var manufactureWeek = ParseManufactureWeek(edidData) ?? 0; + var manufactureYear = ParseManufactureYear(edidData) ?? 0; + var edidVersion = ParseEdidVersion(edidData); + var videoInput = ParseVideoInputDefinition(edidData); + var chromaticity = ParseChromaticity(edidData); + + var colorInfo = new EdidColorInfo( + (float)(chromaticity?.White.X ?? 0.0), + (float)(chromaticity?.White.Y ?? 0.0), + (float)(chromaticity?.Red.X ?? 0.0), + (float)(chromaticity?.Red.Y ?? 0.0), + (float)(chromaticity?.Green.X ?? 0.0), + (float)(chromaticity?.Green.Y ?? 0.0), + (float)(chromaticity?.Blue.X ?? 0.0), + (float)(chromaticity?.Blue.Y ?? 0.0) + ); + + return new ParsedEdidInfo( + manufacturerName, + manufacturerCode, + productCode, + serialNumber, + manufactureWeek, + manufactureYear, + edidVersion?.Major ?? 0, + edidVersion?.Minor ?? 0, + videoInput?.ToString() ?? "Unknown", + colorInfo, + edidData, + isFromActiveEntry + ); + } + catch + { + return null; + } + } + + /// + /// Finds all registry entries for a given hardware ID. + /// + /// Hardware ID to search for (format: ManufacturerKey\InstanceKey) + /// List of all registry entries found for this hardware ID + public static List FindAllRegistryEntries(string hardwareId) + { + var entries = new List(); + + try + { + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return entries; + + // Parse hardware ID (format: ManufacturerKey\InstanceKey) + var parts = hardwareId.Split('\\'); + if (parts.Length != 2) return entries; + + string mfgKey = parts[0]; + string targetInstanceKey = parts[1]; + + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) return entries; + + // Look for the specific instance and similar instances (for conflict detection) + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + // Include exact match and similar instances (same base hardware ID) + if (instanceKey == targetInstanceKey || + instanceKey.StartsWith(targetInstanceKey.Split('&')[0], StringComparison.OrdinalIgnoreCase)) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); + if (deviceParams == null) continue; + + // Read EDID data + if (deviceParams.GetValue("EDID") is not byte[] edidData || edidData.Length < 128) + continue; + + // Validate EDID header + if (!ValidateHeader(edidData)) + continue; + + // Get registry entry metadata + var registryPath = $@"{displayKey}\{mfgKey}\{instanceKey}"; + var lastWriteTime = GetRegistryKeyLastWriteTime(instanceSubKey) ?? DateTime.MinValue; + + entries.Add(new RegistryEdidEntry( + registryPath, + edidData, + lastWriteTime, + instanceKey == targetInstanceKey // Mark as active if exact match + )); + } + } + } + catch + { + // Return partial results on error + } + + return entries; + } + + /// + /// Finds the active registry entry for a hardware ID using physical monitor handle. + /// + /// Hardware ID to search for + /// Physical monitor handle for cross-referencing + /// Active registry entry or null if not found + public static RegistryEdidEntry? FindActiveRegistryEntry(string hardwareId, IntPtr physicalMonitorHandle) + { + var allEntries = FindAllRegistryEntries(hardwareId); + if (allEntries.Count == 0) + return null; + + // If only one entry, assume it's active + if (allEntries.Count == 1) + { + var entry = allEntries[0]; + return entry with { IsCurrentlyActive = true }; + } + + // Multiple entries - use heuristics to determine active one + var activeEntry = ResolveRegistryConflicts(allEntries, physicalMonitorHandle); + if (activeEntry != null) + { + return activeEntry with { IsCurrentlyActive = true }; + } + + // Fallback: return most recently modified entry + var mostRecent = allEntries.OrderByDescending(e => e.LastWriteTime).First(); + return mostRecent with { IsCurrentlyActive = true }; + } + + /// + /// Resolves conflicts when multiple registry entries exist for the same hardware ID. + /// + /// List of conflicting registry entries + /// Physical monitor handle for cross-referencing + /// The most likely active entry or null if cannot be determined + private static RegistryEdidEntry? ResolveRegistryConflicts(List entries, IntPtr physicalMonitorHandle) + { + if (entries.Count <= 1) + return entries.FirstOrDefault(); + + // Strategy 1: Use physical monitor handle to cross-reference with registry + // This is a simplified approach - in a full implementation, we would use + // Windows APIs to map physical monitor handles to registry entries + + // Strategy 2: Prefer entries with more recent timestamps + var recentEntries = entries + .Where(e => e.LastWriteTime > DateTime.Now.AddDays(-30)) // Within last 30 days + .OrderByDescending(e => e.LastWriteTime) + .ToList(); + + if (recentEntries.Count > 0) + return recentEntries.First(); + + // Strategy 3: Prefer entries with valid EDID data and complete information + var validEntries = entries + .Where(e => HasCompleteEdidInfo(e.EdidData)) + .OrderByDescending(e => e.LastWriteTime) + .ToList(); + + if (validEntries.Count > 0) + return validEntries.First(); + + // Fallback: return most recent entry + return entries.OrderByDescending(e => e.LastWriteTime).FirstOrDefault(); + } + + /// + /// Checks if EDID data contains complete information. + /// + /// EDID byte array + /// True if EDID contains manufacturer, product, and other key information + private static bool HasCompleteEdidInfo(byte[] edidData) + { + if (edidData == null || edidData.Length < 128) + return false; + + var manufacturerId = ParseManufacturerId(edidData); + var productCode = ParseProductCode(edidData); + var modelName = ParseModelName(edidData); + + return !string.IsNullOrEmpty(manufacturerId) && + productCode.HasValue && + productCode.Value != 0 && + (!string.IsNullOrEmpty(modelName) || ParseNumericSerialNumber(edidData).HasValue); + } + + /// + /// Extracts hardware ID from Windows device name using proper Windows APIs. + /// + /// Device name (e.g., \\.\DISPLAY1) + /// Hardware ID or null if cannot be extracted + private static string? ExtractHardwareIdFromDeviceName(string deviceName) + { + try + { + // For now, we'll use a more sophisticated approach to map device names to hardware IDs + // This involves checking which registry entries correspond to currently active displays + + // Extract display number from device name (e.g., \\.\DISPLAY1 -> 1) + string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); + if (!int.TryParse(displayNum, out int displayIndex)) + return null; + + // Get all currently active display devices and their registry paths + var activeDisplays = GetActiveDisplayDevices(); + + // Find the hardware ID for the display at the given index + if (displayIndex > 0 && displayIndex <= activeDisplays.Count) + { + return activeDisplays[displayIndex - 1]; // Convert to 0-based index + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Gets hardware IDs for all currently active display devices. + /// + /// List of hardware IDs for active displays + private static List GetActiveDisplayDevices() + { + var activeDisplays = new List(); + + try + { + // This is a simplified implementation. In a full implementation, we would use + // EnumDisplayDevices and SetupDi APIs to get the actual hardware IDs. + // For now, we'll use a heuristic based on registry timestamps and validation. + + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return activeDisplays; + + var candidateEntries = new List<(string hardwareId, DateTime lastWrite, byte[] edid)>(); + + // Collect all valid EDID entries with their metadata + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); + if (deviceParams == null) continue; + + var edidData = deviceParams.GetValue("EDID") as byte[]; + if (edidData == null || edidData.Length < 128 || !ValidateHeader(edidData)) + continue; + + // Check if this entry has recent activity (heuristic for active device) + var lastWrite = GetRegistryKeyLastWriteTime(instanceSubKey) ?? DateTime.MinValue; + + // Use a combination of manufacturer key and instance as hardware ID + string hardwareId = $"{mfgKey}\\{instanceKey}"; + + candidateEntries.Add((hardwareId, lastWrite, edidData)); + } + } + + // Sort by last write time (most recent first) and take the most recent entries + // This heuristic assumes that recently modified registry entries correspond to active monitors + var sortedEntries = candidateEntries + .OrderByDescending(e => e.lastWrite) + .Take(10) // Reasonable limit for number of monitors + .ToList(); + + // Further filter by checking for unique EDID signatures + var uniqueEdids = new HashSet(); + foreach (var entry in sortedEntries) + { + // Create a signature from manufacturer ID and product code + var manufacturerId = ParseManufacturerId(entry.edid); + var productCode = ParseProductCode(entry.edid); + var signature = $"{manufacturerId}_{productCode:X4}"; + + if (!uniqueEdids.Contains(signature)) + { + uniqueEdids.Add(signature); + activeDisplays.Add(entry.hardwareId); + } + } + + return activeDisplays; + } + catch + { + return activeDisplays; + } + } + + /// + /// Gets the last write time of a registry key using heuristics. + /// + /// Registry key + /// Last write time or null if cannot be determined + private static DateTime? GetRegistryKeyLastWriteTime(RegistryKey key) + { + try + { + // Since we can't directly get registry key timestamps in .NET without P/Invoke, + // we'll use a heuristic based on the registry structure and common patterns + + // Check if there are any subkeys with timestamps we can infer from + var subKeyNames = key.GetSubKeyNames(); + var valueNames = key.GetValueNames(); + + // If the key has many subkeys or values, it's likely more recent + // This is a rough heuristic - in practice, we'd use RegQueryInfoKey API + + if (subKeyNames.Length > 0 || valueNames.Length > 0) + { + // Assume recent activity if the key has content + return DateTime.Now.AddDays(-1); // Assume modified within last day + } + + // Fallback to a reasonable default + return DateTime.Now.AddDays(-30); + } + catch + { + return DateTime.Now.AddDays(-30); + } + } + + /// + /// Parses manufacturer ID from EDID bytes. + /// + /// EDID data (at least 2 bytes from offset 8) + /// 3-letter manufacturer ID (e.g., "SAM", "DEL") or null if invalid + public static string? ParseManufacturerId(byte[] edid) + { + if (edid.Length < 10) return null; + + try + { + // Manufacturer ID is stored in bytes 8-9 as 3 5-bit characters + // Bit layout: |0|CHAR1|CHAR2|CHAR3| across 2 bytes + ushort id = (ushort)((edid[8] << 8) | edid[9]); + + char char1 = (char)(((id >> 10) & 0x1F) + 'A' - 1); + char char2 = (char)(((id >> 5) & 0x1F) + 'A' - 1); + char char3 = (char)((id & 0x1F) + 'A' - 1); + + return $"{char1}{char2}{char3}"; + } + catch + { + return null; + } + } + + /// + /// Gets the full manufacturer name from 3-letter PNP ID. + /// + /// 3-letter manufacturer ID + /// Full company name or the ID itself if not found + public static string GetManufacturerName(string? manufacturerId) + { + if (string.IsNullOrEmpty(manufacturerId)) return "Unknown"; + + return manufacturerId switch + { + "AAC" => "AcerView", + "ACI" => "Asus Computer Inc", + "ACR" => "Acer Technologies", + "ACT" => "Targa", + "ADI" => "ADI Corporation", + "AIC" => "AG Neovo", + "AMW" => "AMW", + "AOC" => "AOC International", + "API" => "A Plus Info Corporation", + "APP" => "Apple Computer", + "ART" => "ArtMedia", + "AST" => "AST Research", + "AUO" => "AU Optronics", + "BEL" => "Belkin", + "BEN" => "BenQ Corporation", + "BMM" => "BMM", + "BNQ" => "BenQ Corporation", + "BOE" => "BOE Technology", + "CMO" => "Chi Mei Optoelectronics", + "CPL" => "Compal Electronics", + "CPQ" => "Compaq Computer Corporation", + "CTX" => "CTX International", + "DEC" => "Digital Equipment Corporation", + "DEL" => "Dell Inc.", + "DPC" => "Delta Electronics", + "DWE" => "Daewoo Electronics", + "ECS" => "ELITEGROUP Computer Systems", + "EIZ" => "EIZO Corporation", + "ELS" => "ELSA GmbH", + "ENC" => "Eizo Nanao Corporation", + "EPI" => "Envision Peripherals", + "EPH" => "Epiphan Systems Inc.", + "FUJ" => "Fujitsu Siemens Computers", + "FUS" => "Fujitsu Siemens Computers", + "GSM" => "LG Electronics", + "GWY" => "Gateway 2000", + "HEI" => "Hyundai Electronics Industries", + "HIQ" => "Hyundai ImageQuest", + "HIT" => "Hitachi", + "HPN" => "HP Inc.", + "HSD" => "Hannstar Display Corporation", + "HSL" => "Hansol Electronics", + "HTC" => "Hitachi", + "HWP" => "HP Inc.", + "IBM" => "IBM Corporation", + "ICL" => "Fujitsu ICL", + "IFS" => "InFocus Corporation", + "IQT" => "Hyundai ImageQuest", + "IVM" => "Iiyama North America", + "KDS" => "KDS USA", + "KFC" => "KFC Computek", + "LEN" => "Lenovo", + "LGD" => "LG Display", + "LKM" => "ADLAS", + "LNK" => "LINK Technologies", + "LPL" => "LG Philips", + "LTN" => "Lite-On Technology", + "MAG" => "MAG InnoVision", + "MAX" => "Maxdata Computer", + "MEI" => "Panasonic Industry Company", + "MEL" => "Mitsubishi Electronics", + "MED" => "Matsushita Electric Industrial", + "MS_" => "Panasonic Industry Company", + "MSI" => "Micro-Star International", + "MSH" => "Microsoft Corporation", + "NAN" => "NANAO Corporation", + "NEC" => "NEC Corporation", + "NOK" => "Nokia", + "NVD" => "Nvidia", + "OPT" => "Optoma Corporation", + "OQI" => "OPTIQUEST", + "PBN" => "Packard Bell", + "PCK" => "Daewoo Electronics", + "PDC" => "Polaroid", + "PGS" => "Princeton Graphic Systems", + "PHL" => "Philips Consumer Electronics", + "PIX" => "Pixelink", + "PNR" => "Planar Systems", + "PRT" => "Princeton Graphic Systems", + "REL" => "Relisys", + "SAM" => "Samsung Electric Company", + "SAN" => "Sanyo Electric Co.", + "SBI" => "Smarttech", + "SEC" => "Seiko Epson Corporation", + "SGI" => "Silicon Graphics", + "SMC" => "Samtron", + "SMI" => "Smile", + "SNI" => "Siemens Nixdorf", + "SNY" => "Sony Corporation", + "SPT" => "Sceptre Tech Inc.", + "SRC" => "Shamrock Technology", + "STN" => "Samtron", + "STP" => "Sceptre Tech Inc.", + "TAT" => "Tatung Company of America", + "TOS" => "Toshiba Corporation", + "TRL" => "Royal Information Company", + "TSB" => "Toshiba America Info Systems", + "UNK" => "Unknown", + "UNM" => "Unisys Corporation", + "VSC" => "ViewSonic Corporation", + "WTC" => "Wen Technology", + "ZCM" => "Zenith Data Systems", + _ => manufacturerId, + }; + } + + /// + /// Parses the model name from EDID descriptor blocks. + /// + /// EDID data (at least 128 bytes) + /// Model name string or null if not found + public static string? ParseModelName(byte[] edid) + { + if (edid.Length < 128) return null; + + // Check descriptor blocks at offsets 54, 72, 90, 108 (18 bytes each) + for (int offset = 54; offset <= 108; offset += 18) + { + // Descriptor type 0xFC indicates monitor name + if (edid[offset] == 0x00 && edid[offset + 1] == 0x00 && + edid[offset + 2] == 0x00 && edid[offset + 3] == 0xFC) + { + return ParseDescriptorString(edid, offset + 5); + } + } + + return null; + } + + /// + /// Parses the serial number from EDID descriptor blocks. + /// + /// EDID data (at least 128 bytes) + /// Serial number string or null if not found + public static string? ParseSerialNumber(byte[] edid) + { + if (edid.Length < 128) return null; + + // Check descriptor blocks at offsets 54, 72, 90, 108 (18 bytes each) + for (int offset = 54; offset <= 108; offset += 18) + { + // Descriptor type 0xFF indicates serial number + if (edid[offset] == 0x00 && edid[offset + 1] == 0x00 && + edid[offset + 2] == 0x00 && edid[offset + 3] == 0xFF) + { + return ParseDescriptorString(edid, offset + 5); + } + } + + // If no descriptor serial found, try numeric serial at bytes 12-15 + var numericSerial = ParseNumericSerialNumber(edid); + if (numericSerial.HasValue && numericSerial.Value != 0) + { + return numericSerial.Value.ToString(); + } + + return null; + } + + /// + /// Parses the numeric serial number from EDID. + /// + /// EDID data (at least 16 bytes) + /// Numeric serial number or null if invalid + public static uint? ParseNumericSerialNumber(byte[] edid) + { + if (edid.Length < 16) return null; + + try + { + // Serial number is at bytes 12-15 (little-endian, 32-bit) + uint serial = (uint)(edid[12] | (edid[13] << 8) | (edid[14] << 16) | (edid[15] << 24)); + return serial; + } + catch + { + return null; + } + } + + /// + /// Parses the product code from EDID. + /// + /// EDID data (at least 12 bytes) + /// Product code as ushort or null if invalid + public static ushort? ParseProductCode(byte[] edid) + { + if (edid.Length < 12) return null; + + try + { + // Product code is at bytes 10-11 (little-endian) + return (ushort)(edid[10] | (edid[11] << 8)); + } + catch + { + return null; + } + } + + /// + /// Parses the manufacture year from EDID. + /// + /// EDID data (at least 18 bytes) + /// Manufacture year (e.g., 2023) or null if invalid + public static int? ParseManufactureYear(byte[] edid) + { + if (edid.Length < 18) return null; + + try + { + // Manufacture year is at byte 17, stored as offset from 1990 + int year = edid[17] + 1990; + return year >= 1990 && year <= 2100 ? year : null; + } + catch + { + return null; + } + } + + /// + /// Parses the manufacture week from EDID. + /// + /// EDID data (at least 17 bytes) + /// Manufacture week (1-53) or null if invalid + public static int? ParseManufactureWeek(byte[] edid) + { + if (edid.Length < 17) return null; + + try + { + // Manufacture week is at byte 16 (1-53, or 0xFF for unknown) + int week = edid[16]; + return week >= 1 && week <= 53 ? week : null; + } + catch + { + return null; + } + } + + /// + /// Helper to parse ASCII string from EDID descriptor block. + /// + private static string? ParseDescriptorString(byte[] edid, int offset) + { + try + { + var sb = new StringBuilder(); + for (int i = 0; i < 13; i++) + { + byte b = edid[offset + i]; + if (b == 0x0A || b == 0x00) break; // Newline or null terminator + if (b >= 0x20 && b <= 0x7E) // Printable ASCII + { + sb.Append((char)b); + } + } + + string result = sb.ToString().Trim(); + return string.IsNullOrWhiteSpace(result) ? null : result; + } + catch + { + return null; + } + } + + /// + /// Validates EDID header (first 8 bytes should be: 00 FF FF FF FF FF FF 00). + /// + /// EDID data (at least 8 bytes) + /// True if header is valid + public static bool ValidateHeader(byte[] edid) + { + if (edid.Length < 8) return false; + + return edid[0] == 0x00 && + edid[1] == 0xFF && + edid[2] == 0xFF && + edid[3] == 0xFF && + edid[4] == 0xFF && + edid[5] == 0xFF && + edid[6] == 0xFF && + edid[7] == 0x00; + } + + /// + /// Parses EDID version and revision from EDID. + /// + /// EDID data (at least 20 bytes) + /// EDID version information or null if invalid + public static EdidVersion? ParseEdidVersion(byte[] edid) + { + if (edid.Length < 20) return null; + + try + { + // EDID version is at bytes 18-19 + byte major = edid[18]; + byte minor = edid[19]; + return new EdidVersion(major, minor); + } + catch + { + return null; + } + } + + /// + /// Parses video input definition from EDID. + /// + /// EDID data (at least 21 bytes) + /// Video input definition or null if invalid + public static VideoInputDefinition? ParseVideoInputDefinition(byte[] edid) + { + if (edid.Length < 21) return null; + + try + { + // Video input definition is at byte 20 + byte value = edid[20]; + bool isDigital = (value & 0x80) != 0; // Bit 7 + return new VideoInputDefinition(isDigital, value); + } + catch + { + return null; + } + } + + /// + /// Parses supported features from EDID. + /// + /// EDID data (at least 25 bytes) + /// Supported features information or null if invalid + public static SupportedFeatures? ParseSupportedFeatures(byte[] edid) + { + if (edid.Length < 25) return null; + + try + { + // Supported features is at byte 24 + byte value = edid[24]; + + bool dpmsStandby = (value & 0x80) != 0; // Bit 7 + bool dpmsSuspend = (value & 0x40) != 0; // Bit 6 + bool dpmsActiveOff = (value & 0x20) != 0; // Bit 5 + byte displayType = (byte)((value >> 3) & 0x03); // Bits 4-3 + bool defaultColorSpace = (value & 0x04) != 0; // Bit 2 + bool preferredTimingMode = (value & 0x02) != 0; // Bit 1 + bool continuousFrequency = (value & 0x01) != 0; // Bit 0 + + return new SupportedFeatures( + dpmsStandby, + dpmsSuspend, + dpmsActiveOff, + displayType, + defaultColorSpace, + preferredTimingMode, + continuousFrequency, + value); + } + catch + { + return null; + } + } + + /// + /// Parses chromaticity coordinates (color points for red, green, blue, and white) from EDID. + /// + /// EDID data (at least 35 bytes) + /// Chromaticity coordinates or null if invalid + public static ChromaticityCoordinates? ParseChromaticity(byte[] edid) + { + if (edid.Length < 35) return null; + + try + { + // Chromaticity data is stored in bytes 25-34 + // Each coordinate is a 10-bit value split between two bytes + byte lsb = edid[25]; // Low-order bits for red/green/blue/white X + byte lsb2 = edid[26]; // Low-order bits for red/green/blue/white Y + + // Red X/Y: 8 MSB bits in byte 27, 2 LSB bits in bytes 25/26 + int redXRaw = (edid[27] << 2) | ((lsb >> 6) & 0x03); + int redYRaw = (edid[27] << 2) | ((lsb2 >> 6) & 0x03); + + // Green X/Y: 8 MSB bits in byte 28, 2 LSB bits in bytes 25/26 + int greenXRaw = (edid[28] << 2) | ((lsb >> 4) & 0x03); + int greenYRaw = (edid[28] << 2) | ((lsb2 >> 4) & 0x03); + + // Blue X/Y: 8 MSB bits in byte 29, 2 LSB bits in bytes 25/26 + int blueXRaw = (edid[29] << 2) | ((lsb >> 2) & 0x03); + int blueYRaw = (edid[29] << 2) | ((lsb2 >> 2) & 0x03); + + // White X/Y: 8 MSB bits in byte 30, 2 LSB bits in bytes 25/26 + int whiteXRaw = (edid[30] << 2) | (lsb & 0x03); + int whiteYRaw = (edid[30] << 2) | (lsb2 & 0x03); + + // Convert 10-bit values to 0.0-1.0 range + var red = new ColorPoint(redXRaw / 1024.0, redYRaw / 1024.0); + var green = new ColorPoint(greenXRaw / 1024.0, greenYRaw / 1024.0); + var blue = new ColorPoint(blueXRaw / 1024.0, blueYRaw / 1024.0); + var white = new ColorPoint(whiteXRaw / 1024.0, whiteYRaw / 1024.0); + + return new ChromaticityCoordinates(red, green, blue, white); + } + catch + { + return null; + } + } +} diff --git a/DDCSwitch/Hardware/HardwareInspector.cs b/DDCSwitch/Hardware/HardwareInspector.cs new file mode 100644 index 0000000..a8e59dd --- /dev/null +++ b/DDCSwitch/Hardware/HardwareInspector.cs @@ -0,0 +1,483 @@ +using Microsoft.Win32; +using System.Runtime.InteropServices; + +namespace DDCSwitch; + +/// +/// Connection type for monitor hardware +/// +public enum ConnectionType +{ + Unknown, + HDMI, + DisplayPort, + DVI, + VGA, + USBC, + eDP, + LVDS +} + +/// +/// Hardware information for a monitor +/// +public record HardwareInfo( + string GraphicsDriver, + ConnectionType ConnectionType, + string HardwarePath, + bool IsEmbeddedDisplay, + DdcCiStatus DdcCiStatus +); + +/// +/// Provides hardware inspection functionality for monitors +/// +public static class HardwareInspector +{ + /// + /// Inspects monitor hardware and returns comprehensive hardware information + /// + /// Monitor to inspect + /// Device name from Windows (e.g., \\.\DISPLAY1) + /// Hardware information for the monitor + public static HardwareInfo InspectMonitor(Monitor monitor, string deviceName) + { + if (monitor == null || string.IsNullOrEmpty(deviceName)) + { + return new HardwareInfo( + "Unknown", + ConnectionType.Unknown, + "Unknown", + false, + DdcCiStatus.Unknown + ); + } + + try + { + // Get graphics driver information + string graphicsDriver = GetGraphicsDriver(deviceName); + + // Determine connection type + ConnectionType connectionType = DetermineConnectionType(deviceName); + + // Get hardware path information + string hardwarePath = GetHardwarePath(deviceName); + + // Check if this is an embedded display + bool isEmbeddedDisplay = IsEmbeddedDisplay(connectionType, deviceName); + + // Assess DDC/CI responsiveness status + DdcCiStatus ddcCiStatus = AssessDdcCiStatus(monitor); + + return new HardwareInfo( + graphicsDriver, + connectionType, + hardwarePath, + isEmbeddedDisplay, + ddcCiStatus + ); + } + catch (Exception) + { + // Graceful degradation on any error + return new HardwareInfo( + "Unknown", + ConnectionType.Unknown, + deviceName, + false, + DdcCiStatus.Unknown + ); + } + } + + /// + /// Determines the connection type for a monitor + /// + /// Device name from Windows + /// Connection type + public static ConnectionType DetermineConnectionType(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return ConnectionType.Unknown; + + try + { + // Try to get connection information from WMI + var connectionType = GetConnectionTypeFromWmi(deviceName); + if (connectionType != ConnectionType.Unknown) + return connectionType; + + // Try to get connection information from registry + connectionType = GetConnectionTypeFromRegistry(deviceName); + if (connectionType != ConnectionType.Unknown) + return connectionType; + + // Try to infer from device name patterns + return InferConnectionTypeFromDeviceName(deviceName); + } + catch + { + return ConnectionType.Unknown; + } + } + + /// + /// Gets the graphics driver information for a monitor + /// + /// Device name from Windows + /// Graphics driver name and version + public static string GetGraphicsDriver(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return "Unknown"; + + try + { + // Try to get driver information from WMI + var driverInfo = GetDriverInfoFromWmi(deviceName); + if (!string.IsNullOrEmpty(driverInfo)) + return driverInfo; + + // Try to get driver information from registry + driverInfo = GetDriverInfoFromRegistry(deviceName); + if (!string.IsNullOrEmpty(driverInfo)) + return driverInfo; + + return "Unknown"; + } + catch + { + return "Unknown"; + } + } + + /// + /// Gets Windows hardware path information for a monitor + /// + /// Device name from Windows + /// Hardware path string + private static string GetHardwarePath(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return "Unknown"; + + try + { + // Try to get hardware path from WMI + var hardwarePath = GetHardwarePathFromWmi(deviceName); + if (!string.IsNullOrEmpty(hardwarePath)) + return hardwarePath; + + // Fallback to device name + return deviceName; + } + catch + { + return deviceName; + } + } + + /// + /// Determines if the display is embedded (laptop screen) + /// + /// Connection type + /// Device name + /// True if embedded display + private static bool IsEmbeddedDisplay(ConnectionType connectionType, string deviceName) + { + // eDP and LVDS are typically embedded displays + if (connectionType == ConnectionType.eDP || connectionType == ConnectionType.LVDS) + return true; + + try + { + // Check for laptop indicators in WMI + return CheckForEmbeddedDisplayInWmi(deviceName); + } + catch + { + return false; + } + } + + /// + /// Assesses DDC/CI responsiveness status for the monitor + /// + /// Monitor to assess + /// DDC/CI status + private static DdcCiStatus AssessDdcCiStatus(Monitor monitor) + { + if (monitor == null) + return DdcCiStatus.Unknown; + + try + { + // Use existing VCP analyzer to determine DDC/CI status + return VcpAnalyzer.TestDdcCiComprehensive(monitor); + } + catch + { + return DdcCiStatus.Unknown; + } + } + + /// + /// Gets connection type from WMI + /// + private static ConnectionType GetConnectionTypeFromWmi(string deviceName) + { + // WMI is not compatible with NativeAOT, use registry-based approach instead + return GetConnectionTypeFromRegistry(deviceName); + } + + /// + /// Gets connection type from registry + /// + private static ConnectionType GetConnectionTypeFromRegistry(string deviceName) + { + try + { + // Look in display registry for connection information + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return ConnectionType.Unknown; + + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + // Check hardware ID for connection type indicators + var hardwareId = instanceSubKey.GetValue("HardwareID") as string[]; + if (hardwareId != null) + { + foreach (var id in hardwareId) + { + var idUpper = id.ToUpperInvariant(); + + if (idUpper.Contains("HDMI")) + return ConnectionType.HDMI; + if (idUpper.Contains("DISPLAYPORT") || idUpper.Contains("DP")) + return ConnectionType.DisplayPort; + if (idUpper.Contains("DVI")) + return ConnectionType.DVI; + if (idUpper.Contains("VGA")) + return ConnectionType.VGA; + if (idUpper.Contains("EDP")) + return ConnectionType.eDP; + if (idUpper.Contains("LVDS")) + return ConnectionType.LVDS; + } + } + } + } + } + catch + { + // Registry access error + } + + return ConnectionType.Unknown; + } + + /// + /// Infers connection type from device name patterns + /// + private static ConnectionType InferConnectionTypeFromDeviceName(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return ConnectionType.Unknown; + + var nameUpper = deviceName.ToUpperInvariant(); + + // Check for common patterns in device names + if (nameUpper.Contains("HDMI")) + return ConnectionType.HDMI; + if (nameUpper.Contains("DP") || nameUpper.Contains("DISPLAYPORT")) + return ConnectionType.DisplayPort; + if (nameUpper.Contains("DVI")) + return ConnectionType.DVI; + if (nameUpper.Contains("VGA")) + return ConnectionType.VGA; + if (nameUpper.Contains("EDP")) + return ConnectionType.eDP; + if (nameUpper.Contains("LVDS")) + return ConnectionType.LVDS; + + return ConnectionType.Unknown; + } + + /// + /// Gets driver information from WMI + /// + private static string GetDriverInfoFromWmi(string deviceName) + { + // WMI is not compatible with NativeAOT, use registry-based approach instead + return GetDriverInfoFromRegistry(deviceName); + } + + /// + /// Gets driver information from registry + /// + private static string GetDriverInfoFromRegistry(string deviceName) + { + try + { + // Look for video controller information in registry + const string videoKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; + using var videoRoot = Registry.LocalMachine.OpenSubKey(videoKey); + if (videoRoot == null) return string.Empty; + + foreach (string subKeyName in videoRoot.GetSubKeyNames()) + { + if (subKeyName == "Properties") continue; + + using var subKey = videoRoot.OpenSubKey(subKeyName); + if (subKey == null) continue; + + var driverDesc = subKey.GetValue("DriverDesc")?.ToString(); + var driverVersion = subKey.GetValue("DriverVersion")?.ToString(); + + if (!string.IsNullOrEmpty(driverDesc)) + { + var result = driverDesc; + if (!string.IsNullOrEmpty(driverVersion)) + { + result += $" (v{driverVersion})"; + } + return result; + } + } + } + catch + { + // Registry access error + } + + return string.Empty; + } + + /// + /// Gets hardware path from registry + /// + private static string GetHardwarePathFromRegistry(string deviceName) + { + try + { + // Look in display registry for hardware path information + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return string.Empty; + + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + // Get hardware ID as the hardware path + var hardwareId = instanceSubKey.GetValue("HardwareID") as string[]; + if (hardwareId != null && hardwareId.Length > 0) + { + return hardwareId[0]; + } + + // Fallback to device instance path + var deviceInstancePath = $@"DISPLAY\{mfgKey}\{instanceKey}"; + return deviceInstancePath; + } + } + } + catch + { + // Registry access error + } + + return string.Empty; + } + + /// + /// Checks for embedded display indicators in registry + /// + private static bool CheckForEmbeddedDisplayInRegistry(string deviceName) + { + try + { + // Check for laptop indicators in system information + const string systemKey = @"HARDWARE\DESCRIPTION\System"; + using var systemRoot = Registry.LocalMachine.OpenSubKey(systemKey); + if (systemRoot != null) + { + var systemBiosVersion = systemRoot.GetValue("SystemBiosVersion") as string[]; + if (systemBiosVersion != null) + { + foreach (var version in systemBiosVersion) + { + var versionUpper = version.ToUpperInvariant(); + if (versionUpper.Contains("LAPTOP") || versionUpper.Contains("PORTABLE") || + versionUpper.Contains("NOTEBOOK") || versionUpper.Contains("MOBILE")) + { + return true; + } + } + } + } + + // Check for battery presence in registry (indicates laptop) + const string batteryKey = @"SYSTEM\CurrentControlSet\Services\battery"; + using var batteryRoot = Registry.LocalMachine.OpenSubKey(batteryKey); + if (batteryRoot != null) + { + return true; + } + + // Check for ACPI battery devices + const string acpiKey = @"SYSTEM\CurrentControlSet\Enum\ACPI"; + using var acpiRoot = Registry.LocalMachine.OpenSubKey(acpiKey); + if (acpiRoot != null) + { + foreach (string deviceKey in acpiRoot.GetSubKeyNames()) + { + if (deviceKey.StartsWith("PNP0C0A", StringComparison.OrdinalIgnoreCase)) // Battery device + { + return true; + } + } + } + } + catch + { + // Registry access error + } + + return false; + } + + /// + /// Gets hardware path from WMI + /// + private static string GetHardwarePathFromWmi(string deviceName) + { + // WMI is not compatible with NativeAOT, use registry-based approach instead + return GetHardwarePathFromRegistry(deviceName); + } + + /// + /// Checks for embedded display indicators in WMI + /// + private static bool CheckForEmbeddedDisplayInWmi(string deviceName) + { + // WMI is not compatible with NativeAOT, use registry-based approach instead + return CheckForEmbeddedDisplayInRegistry(deviceName); + } +} \ No newline at end of file diff --git a/DDCSwitch/Identification/DdcCiMonitorIdentifier.cs b/DDCSwitch/Identification/DdcCiMonitorIdentifier.cs new file mode 100644 index 0000000..bb16f97 --- /dev/null +++ b/DDCSwitch/Identification/DdcCiMonitorIdentifier.cs @@ -0,0 +1,326 @@ +using System.Text; + +namespace DDCSwitch; + +/// +/// Information about monitor identity obtained via DDC/CI +/// +public record MonitorIdentityInfo( + string? ControllerManufacturer, + string? FirmwareLevel, + string? ControllerVersion, + string? CapabilitiesString, + Dictionary IdentificationVcpValues, + bool IsFromDdcCi +); + +/// +/// Provides DDC/CI-based monitor identification functionality +/// +public static class DdcCiMonitorIdentifier +{ + // VCP codes for monitor identification + private const byte VCP_CONTROLLER_MANUFACTURER = 0xC4; + private const byte VCP_FIRMWARE_LEVEL = 0xC2; + private const byte VCP_CONTROLLER_VERSION = 0xC8; + private const byte VCP_CAPABILITIES_REQUEST = 0xF3; + + /// + /// Attempts to get monitor identity information via DDC/CI + /// + /// Monitor to query + /// Monitor identity information or null if DDC/CI is not available + public static MonitorIdentityInfo? GetIdentityViaDdcCi(Monitor monitor) + { + if (monitor == null) + { + return null; + } + + var identificationValues = new Dictionary(); + + // Try to get controller manufacturer + string? controllerManufacturer = GetControllerManufacturer(monitor); + if (controllerManufacturer != null && monitor.TryGetVcpFeature(VCP_CONTROLLER_MANUFACTURER, out uint mfgValue, out _)) + { + identificationValues[VCP_CONTROLLER_MANUFACTURER] = mfgValue; + } + + // Try to get firmware level + string? firmwareLevel = GetFirmwareLevel(monitor); + if (firmwareLevel != null && monitor.TryGetVcpFeature(VCP_FIRMWARE_LEVEL, out uint fwValue, out _)) + { + identificationValues[VCP_FIRMWARE_LEVEL] = fwValue; + } + + // Try to get controller version + string? controllerVersion = GetControllerVersion(monitor); + if (controllerVersion != null && monitor.TryGetVcpFeature(VCP_CONTROLLER_VERSION, out uint verValue, out _)) + { + identificationValues[VCP_CONTROLLER_VERSION] = verValue; + } + + // Try to get capabilities string + string? capabilitiesString = GetCapabilitiesString(monitor); + + // If we got any DDC/CI data, return the identity info + if (controllerManufacturer != null || firmwareLevel != null || + controllerVersion != null || capabilitiesString != null) + { + return new MonitorIdentityInfo( + controllerManufacturer, + firmwareLevel, + controllerVersion, + capabilitiesString, + identificationValues, + true + ); + } + + return null; + } + + /// + /// Attempts to retrieve DDC/CI capabilities string from the monitor + /// + /// Monitor to query + /// Capabilities string or null if not available + public static string? GetCapabilitiesString(Monitor monitor) + { + if (monitor == null) + return null; + + try + { + // DDC/CI capabilities string retrieval is complex and requires special handling + // For now, we'll attempt to read the capabilities request VCP code + // In a full implementation, this would use GetCapabilitiesString DDC/CI command + + // Try to read capabilities request VCP code as a fallback + if (monitor.TryGetVcpFeature(VCP_CAPABILITIES_REQUEST, out uint capValue, out uint maxValue)) + { + // This is a simplified approach - real capabilities string retrieval + // would require implementing the full DDC/CI capabilities protocol + return $"(caps,mccs_ver(2.1),vcp({capValue:X2}))"; + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Attempts to get controller manufacturer information via VCP code 0xC4 + /// + /// Monitor to query + /// Controller manufacturer string or null if not available + public static string? GetControllerManufacturer(Monitor monitor) + { + if (monitor == null) + return null; + + try + { + if (monitor.TryGetVcpFeature(VCP_CONTROLLER_MANUFACTURER, out uint value, out _)) + { + // Convert the value to a manufacturer string + // The format varies by manufacturer, but often uses ASCII encoding + return DecodeManufacturerValue(value); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Attempts to get firmware level information via VCP code 0xC2 + /// + /// Monitor to query + /// Firmware level string or null if not available + public static string? GetFirmwareLevel(Monitor monitor) + { + if (monitor == null) + return null; + + try + { + if (monitor.TryGetVcpFeature(VCP_FIRMWARE_LEVEL, out uint value, out _)) + { + // Firmware level is typically encoded as a version number + // Format varies by manufacturer but often uses BCD or simple numeric encoding + return DecodeFirmwareLevel(value); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Attempts to get controller version information via VCP code 0xC8 + /// + /// Monitor to query + /// Controller version string or null if not available + public static string? GetControllerVersion(Monitor monitor) + { + if (monitor == null) + return null; + + try + { + if (monitor.TryGetVcpFeature(VCP_CONTROLLER_VERSION, out uint value, out _)) + { + // Controller version is typically encoded as a version number + return DecodeControllerVersion(value); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Decodes manufacturer value from VCP code 0xC4 + /// + private static string? DecodeManufacturerValue(uint value) + { + if (value == 0) + return null; + + try + { + // Try to decode as ASCII characters (common format) + var bytes = new List(); + + // Extract bytes from the 32-bit value + for (int i = 0; i < 4; i++) + { + byte b = (byte)((value >> (i * 8)) & 0xFF); + if (b != 0 && b >= 0x20 && b <= 0x7E) // Printable ASCII + { + bytes.Add(b); + } + } + + if (bytes.Count > 0) + { + bytes.Reverse(); // Most significant byte first + return Encoding.ASCII.GetString(bytes.ToArray()).Trim(); + } + + // If ASCII decoding fails, return as hex value + return $"0x{value:X8}"; + } + catch + { + return $"0x{value:X8}"; + } + } + + /// + /// Decodes firmware level from VCP code 0xC2 + /// + private static string? DecodeFirmwareLevel(uint value) + { + if (value == 0) + return null; + + try + { + // Common formats: + // - BCD encoding: 0x0123 = version 1.23 + // - Simple numeric: 0x001A = version 26 + // - ASCII encoding: similar to manufacturer + + // Try BCD decoding first (most common) + if (value <= 0xFFFF) + { + uint major = (value >> 8) & 0xFF; + uint minor = value & 0xFF; + + // Check if it looks like BCD + if ((major & 0xF0) <= 0x90 && (major & 0x0F) <= 0x09 && + (minor & 0xF0) <= 0x90 && (minor & 0x0F) <= 0x09) + { + uint majorBcd = ((major >> 4) * 10) + (major & 0x0F); + uint minorBcd = ((minor >> 4) * 10) + (minor & 0x0F); + return $"{majorBcd}.{minorBcd:D2}"; + } + + // Try simple major.minor format + if (major > 0 && major <= 99 && minor <= 99) + { + return $"{major}.{minor:D2}"; + } + } + + // Try ASCII decoding + var asciiResult = DecodeManufacturerValue(value); + if (asciiResult != null && !asciiResult.StartsWith("0x")) + { + return asciiResult; + } + + // Fallback to hex representation + return $"0x{value:X4}"; + } + catch + { + return $"0x{value:X4}"; + } + } + + /// + /// Decodes controller version from VCP code 0xC8 + /// + private static string? DecodeControllerVersion(uint value) + { + if (value == 0) + return null; + + try + { + // Similar to firmware level but may have different encoding + // Some monitors use this for hardware revision + + if (value <= 0xFFFF) + { + uint high = (value >> 8) & 0xFF; + uint low = value & 0xFF; + + // Try simple version format + if (high > 0 && high <= 99) + { + return $"{high}.{low}"; + } + } + + // Try ASCII decoding + var asciiResult = DecodeManufacturerValue(value); + if (asciiResult != null && !asciiResult.StartsWith("0x")) + { + return asciiResult; + } + + // Fallback to hex representation + return $"0x{value:X4}"; + } + catch + { + return $"0x{value:X4}"; + } + } +} \ No newline at end of file diff --git a/DDCSwitch/Identification/MonitorNameResolver.cs b/DDCSwitch/Identification/MonitorNameResolver.cs new file mode 100644 index 0000000..186df9f --- /dev/null +++ b/DDCSwitch/Identification/MonitorNameResolver.cs @@ -0,0 +1,203 @@ +namespace DDCSwitch; + +/// +/// Resolves monitor names with DDC/CI priority over EDID registry data +/// +public static class MonitorNameResolver +{ + /// + /// Resolves the best available monitor name using DDC/CI first, then EDID, then Windows fallback + /// + /// Monitor instance + /// DDC/CI identity information (if available) + /// EDID information (if available) + /// Resolved monitor name + public static string ResolveMonitorName(Monitor monitor, MonitorIdentityInfo? ddcCiIdentity, ParsedEdidInfo? edidInfo) + { + // Priority 1: DDC/CI Controller Manufacturer + Capabilities + if (HasNameFromDdcCi(ddcCiIdentity)) + { + var ddcCiName = BuildDdcCiName(ddcCiIdentity!); + if (!string.IsNullOrEmpty(ddcCiName)) + { + return ddcCiName; + } + } + + // Priority 2: EDID Manufacturer + Model + if (HasNameFromEdid(edidInfo)) + { + var edidName = BuildEdidName(edidInfo!); + if (!string.IsNullOrEmpty(edidName)) + { + return edidName; + } + } + + // Priority 3: Windows Physical Monitor Description (fallback) + return GetFallbackName(monitor); + } + + /// + /// Checks if DDC/CI provides sufficient information for naming + /// + /// DDC/CI identity information + /// True if DDC/CI can provide a name + public static bool HasNameFromDdcCi(MonitorIdentityInfo? ddcCiIdentity) + { + return ddcCiIdentity != null && + ddcCiIdentity.IsFromDdcCi && + (!string.IsNullOrEmpty(ddcCiIdentity.ControllerManufacturer) || + !string.IsNullOrEmpty(ddcCiIdentity.CapabilitiesString)); + } + + /// + /// Checks if EDID provides sufficient information for naming + /// + /// EDID information + /// True if EDID can provide a name + public static bool HasNameFromEdid(ParsedEdidInfo? edidInfo) + { + return edidInfo != null && + (!string.IsNullOrEmpty(edidInfo.ManufacturerName) || + !string.IsNullOrEmpty(edidInfo.ManufacturerCode)); + } + + /// + /// Gets fallback name from Windows monitor description + /// + /// Monitor instance + /// Fallback name + public static string GetFallbackName(Monitor monitor) + { + if (!string.IsNullOrEmpty(monitor.Name) && monitor.Name != "Generic PnP Monitor") + { + return monitor.Name; + } + + return $"Monitor {monitor.Index}"; + } + + /// + /// Builds a monitor name from DDC/CI information + /// + /// DDC/CI identity information + /// DDC/CI-derived monitor name + private static string BuildDdcCiName(MonitorIdentityInfo ddcCiIdentity) + { + var nameParts = new List(); + + // Add controller manufacturer if available + if (!string.IsNullOrEmpty(ddcCiIdentity.ControllerManufacturer)) + { + nameParts.Add(ddcCiIdentity.ControllerManufacturer); + } + + // Try to extract model information from capabilities string + var modelFromCaps = ExtractModelFromCapabilities(ddcCiIdentity.CapabilitiesString); + if (!string.IsNullOrEmpty(modelFromCaps)) + { + nameParts.Add(modelFromCaps); + } + + // Add firmware/version info if available and no model found + if (nameParts.Count == 1) // Only manufacturer so far + { + if (!string.IsNullOrEmpty(ddcCiIdentity.FirmwareLevel)) + { + nameParts.Add($"FW{ddcCiIdentity.FirmwareLevel}"); + } + else if (!string.IsNullOrEmpty(ddcCiIdentity.ControllerVersion)) + { + nameParts.Add($"v{ddcCiIdentity.ControllerVersion}"); + } + } + + return nameParts.Count > 0 ? string.Join(" ", nameParts) : string.Empty; + } + + /// + /// Builds a monitor name from EDID information + /// + /// EDID information + /// EDID-derived monitor name + private static string BuildEdidName(ParsedEdidInfo edidInfo) + { + var nameParts = new List(); + + // Add manufacturer name (prefer full name over code) + if (!string.IsNullOrEmpty(edidInfo.ManufacturerName) && + edidInfo.ManufacturerName != edidInfo.ManufacturerCode) + { + nameParts.Add(edidInfo.ManufacturerName); + } + else if (!string.IsNullOrEmpty(edidInfo.ManufacturerCode)) + { + nameParts.Add(edidInfo.ManufacturerCode); + } + + // Try to get model name from raw EDID data + var modelName = EdidParser.ParseModelName(edidInfo.RawData); + if (!string.IsNullOrEmpty(modelName)) + { + nameParts.Add(modelName); + } + else if (edidInfo.ProductCode != 0) + { + // Fallback to product code if no model name + nameParts.Add($"0x{edidInfo.ProductCode:X4}"); + } + + return nameParts.Count > 0 ? string.Join(" ", nameParts) : string.Empty; + } + + /// + /// Attempts to extract model information from DDC/CI capabilities string + /// + /// DDC/CI capabilities string + /// Model name if found, null otherwise + private static string? ExtractModelFromCapabilities(string? capabilitiesString) + { + if (string.IsNullOrEmpty(capabilitiesString)) + return null; + + try + { + // DDC/CI capabilities strings often contain model information + // Format varies but may include model names or codes + // Example: "(prot(monitor)type(LCD)model(VG248QE)cmds(01 02 03 07 0C E3 F3)vcp(10 12 14(05 08 0B) 16 18 1A 52 60(11 12) AC AE B2 B6 C6 C8 C9 D6(01 04) DF)mswhql(1))" + + // Look for model() tag + var modelMatch = System.Text.RegularExpressions.Regex.Match( + capabilitiesString, + @"model\(([^)]+)\)", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + if (modelMatch.Success) + { + return modelMatch.Groups[1].Value.Trim(); + } + + // Look for type() tag as fallback + var typeMatch = System.Text.RegularExpressions.Regex.Match( + capabilitiesString, + @"type\(([^)]+)\)", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + if (typeMatch.Success) + { + var typeValue = typeMatch.Groups[1].Value.Trim(); + if (typeValue != "LCD" && typeValue != "CRT") // Skip generic types + { + return typeValue; + } + } + + return null; + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/DDCSwitch/JsonContext.cs b/DDCSwitch/JsonContext.cs index 9ea4b3b..b40569a 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -14,9 +14,19 @@ namespace DDCSwitch; [JsonSerializable(typeof(MonitorReference))] [JsonSerializable(typeof(MonitorInfoResponse))] [JsonSerializable(typeof(EdidInfo))] +[JsonSerializable(typeof(ParsedEdidInfo))] +[JsonSerializable(typeof(RegistryEdidEntry))] +[JsonSerializable(typeof(EdidColorInfo))] [JsonSerializable(typeof(FeaturesInfo))] [JsonSerializable(typeof(ChromaticityInfo))] [JsonSerializable(typeof(ColorPointInfo))] +[JsonSerializable(typeof(MonitorIdentityInfo))] +[JsonSerializable(typeof(VcpVersionInfo))] +[JsonSerializable(typeof(VcpTestResult))] +[JsonSerializable(typeof(VcpCapabilityInfo))] +[JsonSerializable(typeof(DdcCiStatus))] +[JsonSerializable(typeof(HardwareInfo))] +[JsonSerializable(typeof(ConnectionType))] [JsonSourceGenerationOptions( WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/NativeMethods.cs deleted file mode 100644 index da45c42..0000000 --- a/DDCSwitch/NativeMethods.cs +++ /dev/null @@ -1,174 +0,0 @@ -๏ปฟusing System; -using System.Runtime.InteropServices; -using Microsoft.Win32; - -namespace DDCSwitch; - -internal static class NativeMethods -{ - // Monitor enumeration - [DllImport("user32.dll")] - public static extern bool EnumDisplayMonitors( - IntPtr hdc, - IntPtr lprcClip, - MonitorEnumProc lpfnEnum, - IntPtr dwData); - - public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); - - [StructLayout(LayoutKind.Sequential)] - public struct RECT - { - public int Left; - public int Top; - public int Right; - public int Bottom; - } - - // Physical monitor structures - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct PHYSICAL_MONITOR - { - public IntPtr hPhysicalMonitor; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] - public string szPhysicalMonitorDescription; - } - - // DDC/CI functions from dxva2.dll - [DllImport("dxva2.dll", SetLastError = true)] - public static extern bool GetNumberOfPhysicalMonitorsFromHMONITOR( - IntPtr hMonitor, - out uint pdwNumberOfPhysicalMonitors); - - [DllImport("dxva2.dll", SetLastError = true)] - public static extern bool GetPhysicalMonitorsFromHMONITOR( - IntPtr hMonitor, - uint dwPhysicalMonitorArraySize, - [Out] PHYSICAL_MONITOR[] pPhysicalMonitorArray); - - [DllImport("dxva2.dll", SetLastError = true)] - public static extern bool GetVCPFeatureAndVCPFeatureReply( - IntPtr hMonitor, - byte bVCPCode, - out uint pvct, - out uint pdwCurrentValue, - out uint pdwMaximumValue); - - [DllImport("dxva2.dll", SetLastError = true)] - public static extern bool SetVCPFeature( - IntPtr hMonitor, - byte bVCPCode, - uint dwNewValue); - - [DllImport("dxva2.dll", SetLastError = true)] - public static extern bool DestroyPhysicalMonitor(IntPtr hMonitor); - - [DllImport("dxva2.dll", SetLastError = true)] - public static extern bool DestroyPhysicalMonitors( - uint dwPhysicalMonitorArraySize, - PHYSICAL_MONITOR[] pPhysicalMonitorArray); - - // Monitor info structures - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi); - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct MONITORINFOEX - { - public uint cbSize; - public RECT rcMonitor; - public RECT rcWork; - public uint dwFlags; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] - public string szDevice; - } - - public const uint MONITORINFOF_PRIMARY = 0x00000001; - - /// - /// Attempts to retrieve EDID data for a monitor from Windows Registry. - /// - /// Device name from MONITORINFOEX (e.g., \\.\DISPLAY1) - /// EDID byte array or null if not found - public static byte[]? GetEdidFromRegistry(string deviceName) - { - try - { - // Enumerate all DISPLAY devices in registry - const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; - using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); - if (displayRoot == null) return null; - - // Collect all EDIDs from active monitors - var edidList = new List(); - - // Try each monitor manufacturer key - foreach (string mfgKey in displayRoot.GetSubKeyNames()) - { - using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); - if (mfgSubKey == null) continue; - - // Try each instance under this manufacturer - foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) - { - using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); - if (instanceSubKey == null) continue; - - // Check if this is an active device - using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); - if (deviceParams == null) continue; - - // Read EDID data - var edidData = deviceParams.GetValue("EDID") as byte[]; - if (edidData != null && edidData.Length >= 128) - { - // Validate EDID header - if (EdidParser.ValidateHeader(edidData)) - { - edidList.Add(edidData); - } - } - } - } - - // Extract display index from device name (e.g., \\.\DISPLAY1 -> 0-based index 0) - string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); - if (!int.TryParse(displayNum, out int displayIndex)) - return null; - - // Convert 1-based display number to 0-based index - int listIndex = displayIndex - 1; - - // Return EDID at the corresponding index if available - if (listIndex >= 0 && listIndex < edidList.Count) - { - return edidList[listIndex]; - } - - // No reliable EDID mapping found for this display index - return null; - } - catch (System.Security.SecurityException) - { - // No access to registry - return null; - } - catch (System.UnauthorizedAccessException) - { - // Access denied - return null; - } - catch (System.IO.IOException) - { - // Registry I/O error - return null; - } - catch (Exception) - { - // Unexpected error - return null; - } - } -} diff --git a/DDCSwitch/FeatureResolver.cs b/DDCSwitch/VcpFeatures/FeatureResolver.cs similarity index 100% rename from DDCSwitch/FeatureResolver.cs rename to DDCSwitch/VcpFeatures/FeatureResolver.cs diff --git a/DDCSwitch/InputSource.cs b/DDCSwitch/VcpFeatures/InputSource.cs similarity index 100% rename from DDCSwitch/InputSource.cs rename to DDCSwitch/VcpFeatures/InputSource.cs diff --git a/DDCSwitch/VcpFeatures/VcpAnalyzer.cs b/DDCSwitch/VcpFeatures/VcpAnalyzer.cs new file mode 100644 index 0000000..e58804a --- /dev/null +++ b/DDCSwitch/VcpFeatures/VcpAnalyzer.cs @@ -0,0 +1,389 @@ +namespace DDCSwitch; + +/// +/// DDC/CI communication status levels +/// +public enum DdcCiStatus +{ + /// + /// Monitor responds to all tested VCP codes + /// + FullyResponsive, + + /// + /// Monitor responds to some but not all VCP codes + /// + PartiallyResponsive, + + /// + /// Monitor does not respond to any VCP codes + /// + NonResponsive, + + /// + /// Communication error occurred during testing + /// + CommunicationError, + + /// + /// Status could not be determined + /// + Unknown +} + +/// +/// VCP version information +/// +public record VcpVersionInfo( + byte MajorVersion, + byte MinorVersion, + bool IsValid +) +{ + public override string ToString() => IsValid ? $"{MajorVersion}.{MinorVersion}" : "Unknown"; +} + +/// +/// Result of testing a specific VCP code +/// +public record VcpTestResult( + byte VcpCode, + string FeatureName, + bool Success, + uint CurrentValue, + uint MaxValue, + string? ErrorMessage +); + +/// +/// Comprehensive VCP capability information +/// +public record VcpCapabilityInfo( + VcpVersionInfo Version, + string? ControllerManufacturer, + string? FirmwareVersion, + Dictionary SupportedFeatures, + bool SupportsNullResponse, + DdcCiStatus DdcCiStatus, + List TestResults +); + +/// +/// Provides comprehensive VCP analysis and DDC/CI capability testing +/// +public static class VcpAnalyzer +{ + // VCP codes for comprehensive testing + private static readonly byte[] TestVcpCodes = new byte[] + { + 0x10, // Brightness + 0x12, // Contrast + 0x60, // Input Source + 0x62, // Audio Volume + 0x8D, // Audio Mute + 0xDF, // VCP Version + 0xC2, // Firmware Level + 0xC4, // Controller Manufacturer + 0xC8 // Controller Version + }; + + // Retry configuration + private const int MaxRetries = 3; + private const int RetryDelayMs = 50; + + /// + /// Performs comprehensive analysis of monitor VCP capabilities + /// + /// Monitor to analyze + /// Comprehensive VCP capability information + public static VcpCapabilityInfo AnalyzeCapabilities(Monitor monitor) + { + if (monitor == null) + { + return new VcpCapabilityInfo( + new VcpVersionInfo(0, 0, false), + null, + null, + new Dictionary(), + false, + DdcCiStatus.Unknown, + new List() + ); + } + + // Get VCP version information + var vcpVersion = GetVcpVersion(monitor); + + // Get controller information via DDC/CI + var ddcCiIdentity = DdcCiMonitorIdentifier.GetIdentityViaDdcCi(monitor); + string? controllerManufacturer = ddcCiIdentity?.ControllerManufacturer; + string? firmwareVersion = ddcCiIdentity?.FirmwareLevel; + + // Perform comprehensive DDC/CI testing + var ddcCiStatus = TestDdcCiComprehensive(monitor); + + // Test multiple VCP codes for responsiveness + var testResults = new List(); + bool multipleVcpSuccess = TestMultipleVcpCodes(monitor, TestVcpCodes, testResults); + + // Test null response support + bool supportsNullResponse = TestNullResponseSupport(monitor); + + // Get supported features using existing scan functionality + var supportedFeatures = monitor.ScanVcpFeatures(); + + return new VcpCapabilityInfo( + vcpVersion, + controllerManufacturer, + firmwareVersion, + supportedFeatures, + supportsNullResponse, + ddcCiStatus, + testResults + ); + } + + /// + /// Determines VCP version supported by the monitor + /// + /// Monitor to query + /// VCP version information + public static VcpVersionInfo GetVcpVersion(Monitor monitor) + { + if (monitor == null) + return new VcpVersionInfo(0, 0, false); + + try + { + // VCP Version is at code 0xDF + if (monitor.TryGetVcpFeature(0xDF, out uint value, out _)) + { + // VCP version is typically encoded as major.minor in BCD or binary + byte major = (byte)((value >> 8) & 0xFF); + byte minor = (byte)(value & 0xFF); + + // Validate reasonable version numbers + if (major >= 1 && major <= 10 && minor <= 99) + { + return new VcpVersionInfo(major, minor, true); + } + + // Try BCD decoding + if ((major & 0xF0) <= 0x90 && (major & 0x0F) <= 0x09 && + (minor & 0xF0) <= 0x90 && (minor & 0x0F) <= 0x09) + { + byte majorBcd = (byte)(((major >> 4) * 10) + (major & 0x0F)); + byte minorBcd = (byte)(((minor >> 4) * 10) + (minor & 0x0F)); + + if (majorBcd >= 1 && majorBcd <= 10 && minorBcd <= 99) + { + return new VcpVersionInfo(majorBcd, minorBcd, true); + } + } + } + + return new VcpVersionInfo(0, 0, false); + } + catch + { + return new VcpVersionInfo(0, 0, false); + } + } + + /// + /// Performs comprehensive DDC/CI testing to determine communication status + /// + /// Monitor to test + /// DDC/CI communication status + public static DdcCiStatus TestDdcCiComprehensive(Monitor monitor) + { + if (monitor == null) + return DdcCiStatus.Unknown; + + try + { + var testResults = new List(); + bool anySuccess = TestMultipleVcpCodes(monitor, TestVcpCodes, testResults); + + if (!anySuccess) + { + return DdcCiStatus.NonResponsive; + } + + // Count successful tests + int successCount = testResults.Count(r => r.Success); + int totalTests = testResults.Count; + + // Determine status based on success ratio + if (successCount == totalTests) + { + return DdcCiStatus.FullyResponsive; + } + else if (successCount > 0) + { + return DdcCiStatus.PartiallyResponsive; + } + else + { + return DdcCiStatus.NonResponsive; + } + } + catch + { + return DdcCiStatus.CommunicationError; + } + } + + /// + /// Tests multiple VCP codes to determine monitor responsiveness + /// + /// Monitor to test + /// Array of VCP codes to test + /// List to store test results (optional) + /// True if any VCP code responded successfully + public static bool TestMultipleVcpCodes(Monitor monitor, byte[] testCodes, List? results = null) + { + if (monitor == null || testCodes == null) + return false; + + bool anySuccess = false; + + foreach (byte vcpCode in testCodes) + { + var testResult = TestSingleVcpCode(monitor, vcpCode); + results?.Add(testResult); + + if (testResult.Success) + { + anySuccess = true; + } + } + + return anySuccess; + } + + /// + /// Tests a single VCP code with retry logic for timing-sensitive monitors + /// + /// Monitor to test + /// VCP code to test + /// Test result + private static VcpTestResult TestSingleVcpCode(Monitor monitor, byte vcpCode) + { + string featureName = FeatureResolver.GetFeatureByCode(vcpCode).Name; + + for (int attempt = 0; attempt < MaxRetries; attempt++) + { + try + { + if (monitor.TryGetVcpFeature(vcpCode, out uint currentValue, out uint maxValue, out int errorCode)) + { + return new VcpTestResult( + vcpCode, + featureName, + true, + currentValue, + maxValue, + null + ); + } + + // If this is not the last attempt, wait before retrying + if (attempt < MaxRetries - 1) + { + Thread.Sleep(RetryDelayMs); + } + } + catch (Exception ex) + { + if (attempt == MaxRetries - 1) + { + return new VcpTestResult( + vcpCode, + featureName, + false, + 0, + 0, + ex.Message + ); + } + + Thread.Sleep(RetryDelayMs); + } + } + + return new VcpTestResult( + vcpCode, + featureName, + false, + 0, + 0, + "Failed after multiple retries" + ); + } + + /// + /// Tests DDC null response behavior for unsupported features + /// + /// Monitor to test + /// True if monitor supports null responses for unsupported features + private static bool TestNullResponseSupport(Monitor monitor) + { + if (monitor == null) + return false; + + try + { + // Test with a VCP code that's unlikely to be supported (0xFF) + // A monitor that supports null responses should return false without error + // A monitor that doesn't support null responses might hang or return an error + + var startTime = DateTime.UtcNow; + bool result = monitor.TryGetVcpFeature(0xFF, out _, out _); + var elapsed = DateTime.UtcNow - startTime; + + // If the call completed quickly (< 100ms), the monitor likely supports null responses + // If it took longer, the monitor might not handle unsupported codes gracefully + return elapsed.TotalMilliseconds < 100; + } + catch + { + // If an exception occurred, the monitor doesn't handle null responses well + return false; + } + } + + /// + /// Gets a human-readable description of the DDC/CI status + /// + /// DDC/CI status + /// Human-readable description + public static string GetStatusDescription(DdcCiStatus status) + { + return status switch + { + DdcCiStatus.FullyResponsive => "Fully responsive to DDC/CI commands", + DdcCiStatus.PartiallyResponsive => "Partially responsive to DDC/CI commands", + DdcCiStatus.NonResponsive => "Does not respond to DDC/CI commands", + DdcCiStatus.CommunicationError => "Communication error during DDC/CI testing", + DdcCiStatus.Unknown => "DDC/CI status unknown", + _ => "Unknown status" + }; + } + + /// + /// Gets a summary of VCP test results + /// + /// List of test results + /// Summary string + public static string GetTestResultsSummary(List testResults) + { + if (testResults == null || testResults.Count == 0) + return "No tests performed"; + + int successCount = testResults.Count(r => r.Success); + int totalCount = testResults.Count; + + return $"{successCount}/{totalCount} VCP codes responded successfully"; + } +} \ No newline at end of file diff --git a/DDCSwitch/VcpErrorHandler.cs b/DDCSwitch/VcpFeatures/VcpErrorHandler.cs similarity index 100% rename from DDCSwitch/VcpErrorHandler.cs rename to DDCSwitch/VcpFeatures/VcpErrorHandler.cs diff --git a/DDCSwitch/VcpFeature.Generated.cs b/DDCSwitch/VcpFeatures/VcpFeature.Generated.cs similarity index 100% rename from DDCSwitch/VcpFeature.Generated.cs rename to DDCSwitch/VcpFeatures/VcpFeature.Generated.cs diff --git a/DDCSwitch/VcpFeature.cs b/DDCSwitch/VcpFeatures/VcpFeature.cs similarity index 100% rename from DDCSwitch/VcpFeature.cs rename to DDCSwitch/VcpFeatures/VcpFeature.cs diff --git a/EXAMPLES.md b/EXAMPLES.md index dc77bd8..b7aad2a 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,6 +1,8 @@ # ddcswitch Examples -This document contains detailed examples and use cases for ddcswitch, including input switching, brightness/contrast control, comprehensive VCP feature access, EDID information retrieval, and automation. +This document contains detailed examples and use cases for ddcswitch, including input switching, brightness/contrast control, comprehensive VCP feature access, EDID information retrieval, automation, and creative Windows integrations. + +ddcswitch's JSON output support and comprehensive VCP feature access opens up unique automation possibilities. ## Monitor Information (EDID) @@ -1909,3 +1911,1054 @@ Write-Host "Synchronized $($okMonitors.Count) monitors" -ForegroundColor Cyan Happy switching and brightness controlling! +## Creative Windows Integrations + +### Windows Subsystem for Linux (WSL) Integration + +Control Windows monitors from within WSL using ddcswitch: + +```bash +# ~/.bashrc in WSL +alias ddc='/mnt/c/Tools/ddcswitch.exe' +alias ddc-work='ddc set 0 DP1 && ddc set 0 brightness 60%' +alias ddc-game='ddc set 0 HDMI1 && ddc set 0 brightness 90%' + +# Function to get monitor info in WSL +monitor_status() { + /mnt/c/Tools/ddcswitch.exe list --json | jq '.monitors[] | {index, name, currentInput, brightness, contrast}' +} + +# Brightness control from Linux terminal +set_brightness() { + /mnt/c/Tools/ddcswitch.exe set 0 brightness "$1%" + echo "Brightness set to $1%" +} +``` + +### Windows Terminal Custom Actions + +Add ddcswitch commands to Windows Terminal settings.json: + +```json +{ + "actions": [ + { + "command": { + "action": "wt", + "commandline": "ddcswitch set 0 HDMI1" + }, + "keys": "ctrl+alt+h", + "name": "Switch to HDMI" + }, + { + "command": { + "action": "wt", + "commandline": "ddcswitch set 0 DP1" + }, + "keys": "ctrl+alt+d", + "name": "Switch to DisplayPort" + }, + { + "command": { + "action": "wt", + "commandline": "ddcswitch list --verbose" + }, + "keys": "ctrl+alt+m", + "name": "Monitor Status" + } + ] +} +``` + +### Microsoft Power Automate Desktop Integration + +Create automated workflows that respond to system events: + +``` +# Power Automate Desktop Flow: "Smart Monitor Control" + +# Trigger: When specific application launches +IF Application.IsProcessRunning ProcessName: 'GameLauncher' THEN + # Switch to gaming setup + System.RunDOSCommand Command: 'ddcswitch set 0 HDMI1' + System.RunDOSCommand Command: 'ddcswitch set 0 brightness 90%' + System.RunDOSCommand Command: 'ddcswitch set 0 contrast 85%' + + # Show notification + Display.ShowNotification Title: 'Gaming Mode' Message: 'Monitors configured for gaming' +END + +# Trigger: When work hours begin (9 AM) +IF DateTime.Now.Hour = 9 THEN + System.RunDOSCommand Command: 'ddcswitch set 0 DP1' + System.RunDOSCommand Command: 'ddcswitch set 0 brightness 60%' + Display.ShowNotification Title: 'Work Mode' Message: 'Monitors ready for productivity' +END +``` + +### Windows Event Log Integration + +Monitor system events and respond with display changes: + +```powershell +# monitor-event-handler.ps1 +# Register for system events and adjust monitors accordingly + +Register-WmiEvent -Query "SELECT * FROM Win32_VolumeChangeEvent WHERE EventType = 2" -Action { + # USB device connected - might be a console + Start-Sleep -Seconds 2 + $result = ddcswitch list --json | ConvertFrom-Json + + # Check if we should auto-switch to console input + $gameConsoles = @("Xbox", "PlayStation", "Nintendo") + # Logic to detect console and switch input + ddcswitch set 0 HDMI1 + Write-EventLog -LogName Application -Source "ddcswitch" -EventId 1001 -Message "Auto-switched to console input" +} + +# Register for user session changes +Register-WmiEvent -Query "SELECT * FROM Win32_SessionChangeEvent" -Action { + $event = $Event.SourceEventArgs.NewEvent + + switch ($event.Type) { + 7 { # Session locked + ddcswitch set 0 brightness 10% # Dim when locked + } + 8 { # Session unlocked + ddcswitch set 0 brightness 75% # Restore brightness + } + } +} +``` + +### Razer Synapse / Logitech G HUB Integration + +Use macro keys to control monitors: + +```csharp +// Razer Synapse C# Script for macro key +using System.Diagnostics; + +public void ExecuteMacro() +{ + // Gaming profile macro + Process.Start("ddcswitch.exe", "set 0 HDMI1"); + System.Threading.Thread.Sleep(500); + Process.Start("ddcswitch.exe", "set 0 brightness 90%"); + Process.Start("ddcswitch.exe", "set 0 contrast 85%"); +} +``` + +### OBS Studio Integration + +Control monitor settings during streaming: + +```lua +-- OBS Studio Lua Script: monitor-control.lua +obs = obslua + +function script_description() + return "Control monitor settings during streaming" +end + +function on_scene_switch(event) + local scene_name = obs.obs_frontend_get_current_scene_name() + + if scene_name == "Gaming Scene" then + os.execute("ddcswitch set 0 HDMI1") + os.execute("ddcswitch set 0 brightness 95%") + elseif scene_name == "Just Chatting" then + os.execute("ddcswitch set 0 DP1") + os.execute("ddcswitch set 0 brightness 70%") + end +end + +function script_load(settings) + obs.obs_frontend_add_event_callback(on_scene_switch) +end +``` + +### Discord Bot Integration + +Control monitors via Discord commands: + +```javascript +// discord-monitor-bot.js +const { Client, GatewayIntentBits } = require('discord.js'); +const { execSync } = require('child_process'); + +const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] }); + +client.on('messageCreate', async message => { + if (!message.content.startsWith('!monitor')) return; + + const args = message.content.split(' '); + const command = args[1]; + + try { + switch(command) { + case 'status': + const status = execSync('ddcswitch list --json').toString(); + const data = JSON.parse(status); + const embed = { + title: 'Monitor Status', + fields: data.monitors.map(m => ({ + name: m.name, + value: `Input: ${m.currentInput}\nBrightness: ${m.brightness || 'N/A'}`, + inline: true + })) + }; + message.reply({ embeds: [embed] }); + break; + + case 'gaming': + execSync('ddcswitch set 0 HDMI1'); + execSync('ddcswitch set 0 brightness 90%'); + message.reply('๐ŸŽฎ Gaming mode activated!'); + break; + + case 'work': + execSync('ddcswitch set 0 DP1'); + execSync('ddcswitch set 0 brightness 60%'); + message.reply('๐Ÿ’ผ Work mode activated!'); + break; + } + } catch (error) { + message.reply('โŒ Monitor control failed'); + } +}); + +client.login('YOUR_BOT_TOKEN'); +``` + +### Home Assistant Integration + +Control monitors from your smart home system: + +```yaml +# configuration.yaml +shell_command: + monitor_gaming: "ddcswitch set 0 HDMI1 && ddcswitch set 0 brightness 90%" + monitor_work: "ddcswitch set 0 DP1 && ddcswitch set 0 brightness 60%" + monitor_movie: "ddcswitch set 0 HDMI1 && ddcswitch set 0 brightness 30%" + +sensor: + - platform: command_line + name: monitor_status + command: 'ddcswitch list --json' + value_template: '{{ value_json.monitors[0].currentInput }}' + json_attributes: + - monitors + +automation: + - alias: "Gaming Time" + trigger: + platform: time + at: "19:00:00" + action: + service: shell_command.monitor_gaming + + - alias: "Work Hours" + trigger: + platform: time + at: "09:00:00" + action: + service: shell_command.monitor_work +``` + +### Twitch Chat Bot Integration + +Let viewers control your monitor settings: + +```python +# twitch-monitor-bot.py +import socket +import subprocess +import json +import time + +class TwitchBot: + def __init__(self, token, channel): + self.token = token + self.channel = channel + self.sock = socket.socket() + + def connect(self): + self.sock.connect(('irc.chat.twitch.tv', 6667)) + self.sock.send(f"PASS {self.token}\n".encode('utf-8')) + self.sock.send(f"NICK streambot\n".encode('utf-8')) + self.sock.send(f"JOIN {self.channel}\n".encode('utf-8')) + + def send_message(self, message): + self.sock.send(f"PRIVMSG {self.channel} :{message}\n".encode('utf-8')) + + def run_ddc(self, args): + try: + result = subprocess.run(['ddcswitch'] + args + ['--json'], + capture_output=True, text=True) + return json.loads(result.stdout) + except: + return {'success': False} + + def handle_command(self, user, command): + # Only allow certain users or subscribers + if user not in ['streamer', 'moderator']: + return + + if command == '!brightness_up': + # Get current brightness and increase by 10% + current = self.run_ddc(['get', '0', 'brightness']) + if current['success']: + new_brightness = min(100, current['percentageValue'] + 10) + result = self.run_ddc(['set', '0', 'brightness', f'{new_brightness}%']) + if result['success']: + self.send_message(f"Brightness increased to {new_brightness}%") + + elif command == '!brightness_down': + current = self.run_ddc(['get', '0', 'brightness']) + if current['success']: + new_brightness = max(10, current['percentageValue'] - 10) + result = self.run_ddc(['set', '0', 'brightness', f'{new_brightness}%']) + if result['success']: + self.send_message(f"Brightness decreased to {new_brightness}%") + + elif command == '!monitor_status': + status = self.run_ddc(['list', '--verbose']) + if status['success']: + monitor = status['monitors'][0] + self.send_message(f"Monitor: {monitor['currentInput']}, " + f"Brightness: {monitor.get('brightness', 'N/A')}") + +# Usage +bot = TwitchBot('oauth:your_token', '#your_channel') +bot.connect() +# Add message parsing loop... +``` + +### Windows Sandbox Testing Environment + +Test monitor configurations safely: + +```xml + + + + + C:\Tools + C:\Tools + true + + + + C:\Tools\ddcswitch.exe list --verbose + + +``` + +### Microsoft Graph API Integration + +Control monitors based on calendar events: + +```csharp +// CalendarMonitorController.cs +using Microsoft.Graph; +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +public class CalendarMonitorController +{ + private GraphServiceClient _graphClient; + + public async Task CheckUpcomingMeetings() + { + var events = await _graphClient.Me.Events + .Request() + .Filter($"start/dateTime ge '{DateTime.Now:yyyy-MM-ddTHH:mm:ss.fffK}'") + .Top(1) + .GetAsync(); + + if (events.Any()) + { + var nextMeeting = events.First(); + var timeUntilMeeting = nextMeeting.Start.DateTime - DateTime.Now; + + if (timeUntilMeeting.TotalMinutes <= 5) + { + // Meeting starting soon - optimize for video calls + Process.Start("ddcswitch.exe", "set 0 DP1"); // Switch to PC + Process.Start("ddcswitch.exe", "set 0 brightness 80%"); // Good lighting + Process.Start("ddcswitch.exe", "set 0 contrast 75%"); // Clear video + + // Show notification + var notification = new ToastNotification("Meeting Mode", + "Monitor optimized for video call"); + notification.Show(); + } + } + } +} +``` + +### Windows Performance Toolkit Integration + +Monitor performance impact of display changes: + +```powershell +# performance-monitor.ps1 +# Track system performance during monitor operations + +function Measure-MonitorOperation { + param([string]$Operation) + + # Start performance counter + $cpu = Get-Counter "\Processor(_Total)\% Processor Time" + $startTime = Get-Date + + # Execute monitor command + $result = Invoke-Expression "ddcswitch $Operation --json" | ConvertFrom-Json + + $endTime = Get-Date + $duration = ($endTime - $startTime).TotalMilliseconds + + # Log performance data + $perfData = @{ + Operation = $Operation + Success = $result.success + Duration = $duration + CPUBefore = $cpu.CounterSamples[0].CookedValue + Timestamp = $startTime + } + + $perfData | ConvertTo-Json | Out-File -Append "monitor-performance.log" + + Write-Host "Operation: $Operation completed in ${duration}ms" -ForegroundColor Green +} + +# Test various operations +Measure-MonitorOperation "list" +Measure-MonitorOperation "set 0 HDMI1" +Measure-MonitorOperation "set 0 brightness 75%" +Measure-MonitorOperation "get 0" +``` + +### Chocolatey Package Hooks + +Automatically configure monitors when installing/updating software: + +```powershell +# chocolateyinstall.ps1 for a gaming package +$packageName = 'steam' + +# Install Steam normally... +Install-ChocolateyPackage @packageArgs + +# Configure monitors for gaming after Steam install +if (Get-Command ddcswitch -ErrorAction SilentlyContinue) { + Write-Host "Configuring monitors for gaming..." -ForegroundColor Green + + # Create gaming profile + $gamingScript = @" +ddcswitch set 0 HDMI1 +ddcswitch set 0 brightness 90% +ddcswitch set 0 contrast 85% +Write-Host "Gaming profile activated!" -ForegroundColor Green +"@ + + $gamingScript | Out-File "$env:USERPROFILE\Desktop\Gaming-Mode.ps1" + Write-Host "Created Gaming-Mode.ps1 on desktop" -ForegroundColor Cyan +} +``` + +### Windows Subsystem for Android (WSA) Integration + +Control monitors when Android apps launch: + +```bash +#!/system/bin/sh +# monitor-control.sh (in WSA) + +# Detect when gaming apps launch and signal Windows +am monitor --gdb | while read line; do + if echo "$line" | grep -q "com.epicgames.fortnite"; then + # Signal Windows to switch to gaming mode + /mnt/c/Tools/ddcswitch.exe set 0 HDMI1 + /mnt/c/Tools/ddcswitch.exe set 0 brightness 95% + fi +done +``` + +### Microsoft Intune/MDM Integration + +Deploy monitor configurations across enterprise: + +```xml + + + + + 1 + + + ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DODownloadMode + + + int + + 1 + + + + 2 + + + ./Device/Vendor/MSFT/Policy/Config/Update/ScheduledInstallTime + + + chr + + ddcswitch set 0 brightness 60% + + + + +``` + +### Windows Presentation Foundation (WPF) GUI + +Create a visual interface for ddcswitch: + +```csharp +// MonitorControlGUI.xaml.cs +using System; +using System.Diagnostics; +using System.Text.Json; +using System.Windows; +using System.Windows.Controls; + +public partial class MonitorControlWindow : Window +{ + public MonitorControlWindow() + { + InitializeComponent(); + LoadMonitors(); + } + + private void LoadMonitors() + { + try + { + var result = RunDDCCommand("list --json"); + var data = JsonSerializer.Deserialize(result); + + MonitorComboBox.ItemsSource = data.Monitors; + MonitorComboBox.DisplayMemberPath = "Name"; + MonitorComboBox.SelectedValuePath = "Index"; + } + catch (Exception ex) + { + MessageBox.Show($"Failed to load monitors: {ex.Message}"); + } + } + + private void BrightnessSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + if (MonitorComboBox.SelectedValue != null) + { + var monitorIndex = MonitorComboBox.SelectedValue.ToString(); + var brightness = (int)e.NewValue; + + RunDDCCommand($"set {monitorIndex} brightness {brightness}%"); + BrightnessLabel.Content = $"Brightness: {brightness}%"; + } + } + + private void InputButton_Click(object sender, RoutedEventArgs e) + { + var button = sender as Button; + var input = button.Tag.ToString(); + var monitorIndex = MonitorComboBox.SelectedValue.ToString(); + + RunDDCCommand($"set {monitorIndex} {input}"); + StatusLabel.Content = $"Switched to {input}"; + } + + private string RunDDCCommand(string args) + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "ddcswitch.exe", + Arguments = args, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + return output; + } +} +``` + +### Windows Registry Integration + +Store and restore monitor preferences: + +```powershell +# registry-monitor-profiles.ps1 +$registryPath = "HKCU:\Software\ddcswitch\Profiles" + +function Save-MonitorProfile { + param([string]$ProfileName) + + # Get current monitor state + $monitors = ddcswitch list --verbose --json | ConvertFrom-Json + + if (-not (Test-Path $registryPath)) { + New-Item -Path $registryPath -Force + } + + foreach ($monitor in $monitors.monitors) { + $profileKey = "$registryPath\$ProfileName\Monitor$($monitor.index)" + New-Item -Path $profileKey -Force + + Set-ItemProperty -Path $profileKey -Name "Input" -Value $monitor.currentInput + if ($monitor.brightness) { + Set-ItemProperty -Path $profileKey -Name "Brightness" -Value $monitor.brightness + } + if ($monitor.contrast) { + Set-ItemProperty -Path $profileKey -Name "Contrast" -Value $monitor.contrast + } + } + + Write-Host "Profile '$ProfileName' saved to registry" -ForegroundColor Green +} + +function Restore-MonitorProfile { + param([string]$ProfileName) + + $profilePath = "$registryPath\$ProfileName" + if (-not (Test-Path $profilePath)) { + Write-Error "Profile '$ProfileName' not found" + return + } + + $monitorKeys = Get-ChildItem -Path $profilePath + foreach ($key in $monitorKeys) { + $monitorIndex = $key.Name -replace '.*Monitor(\d+)', '$1' + + $input = Get-ItemProperty -Path $key.PSPath -Name "Input" -ErrorAction SilentlyContinue + $brightness = Get-ItemProperty -Path $key.PSPath -Name "Brightness" -ErrorAction SilentlyContinue + $contrast = Get-ItemProperty -Path $key.PSPath -Name "Contrast" -ErrorAction SilentlyContinue + + if ($input) { ddcswitch set $monitorIndex $input.Input } + if ($brightness) { ddcswitch set $monitorIndex brightness $brightness.Brightness } + if ($contrast) { ddcswitch set $monitorIndex contrast $contrast.Contrast } + } + + Write-Host "Profile '$ProfileName' restored" -ForegroundColor Green +} + +# Usage: +# Save-MonitorProfile "Gaming" +# Restore-MonitorProfile "Gaming" +``` + +## Windows Productivity Workflows + +### Focus Mode with Windows Focus Assist + +Combine Windows Focus Assist with monitor dimming for deep work: + +```powershell +# focus-mode.ps1 +param([switch]$Enable, [switch]$Disable) + +if ($Enable) { + # Enable Focus Assist (Priority only) + $registryPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudStore\Store\Cache\DefaultAccount" + # Set Focus Assist to Priority only mode + + # Dim all monitors for focus + $monitors = ddcswitch list --json | ConvertFrom-Json + foreach ($monitor in $monitors.monitors) { + if ($monitor.status -eq "ok") { + ddcswitch set $monitor.index brightness 40% + } + } + + Write-Host "๐ŸŽฏ Focus mode enabled - monitors dimmed, notifications limited" -ForegroundColor Blue + + # Set timer for break reminder + Start-Job -ScriptBlock { + Start-Sleep -Seconds 1800 # 30 minutes + [System.Windows.Forms.MessageBox]::Show("Time for a break!", "Focus Mode") + } +} + +if ($Disable) { + # Restore normal brightness + $monitors = ddcswitch list --json | ConvertFrom-Json + foreach ($monitor in $monitors.monitors) { + if ($monitor.status -eq "ok") { + ddcswitch set $monitor.index brightness 75% + } + } + + Write-Host "โœจ Focus mode disabled - normal brightness restored" -ForegroundColor Green +} +``` + +### Pomodoro Timer Integration + +Automatically adjust monitor settings during work/break cycles: + +```csharp +// PomodoroMonitorController.cs +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +public class PomodoroMonitorController +{ + private Timer _workTimer; + private Timer _breakTimer; + private bool _isWorkSession = true; + + public void StartPomodoro() + { + StartWorkSession(); + } + + private void StartWorkSession() + { + _isWorkSession = true; + + // Work mode: Higher brightness, focus input + RunDDCCommand("set 0 DP1"); // PC input for work + RunDDCCommand("set 0 brightness 80%"); // Good brightness for productivity + RunDDCCommand("set 0 contrast 75%"); // Comfortable contrast + + ShowNotification("๐Ÿ… Work Session Started", "25 minutes of focused work"); + + // Set 25-minute timer + _workTimer = new Timer(OnWorkSessionEnd, null, TimeSpan.FromMinutes(25), Timeout.InfiniteTimeSpan); + } + + private void OnWorkSessionEnd(object state) + { + _workTimer?.Dispose(); + StartBreakSession(); + } + + private void StartBreakSession() + { + _isWorkSession = false; + + // Break mode: Lower brightness, entertainment input + RunDDCCommand("set 0 brightness 50%"); // Dimmer for rest + RunDDCCommand("set 0 contrast 85%"); // Higher contrast for media + + ShowNotification("โ˜• Break Time!", "5 minutes to rest your eyes"); + + // Set 5-minute timer + _breakTimer = new Timer(OnBreakSessionEnd, null, TimeSpan.FromMinutes(5), Timeout.InfiniteTimeSpan); + } + + private void OnBreakSessionEnd(object state) + { + _breakTimer?.Dispose(); + StartWorkSession(); // Loop back to work + } + + private void RunDDCCommand(string args) + { + Process.Start(new ProcessStartInfo + { + FileName = "ddcswitch.exe", + Arguments = args, + CreateNoWindow = true, + UseShellExecute = false + }); + } + + private void ShowNotification(string title, string message) + { + var notification = new NotifyIcon + { + Icon = SystemIcons.Information, + BalloonTipTitle = title, + BalloonTipText = message, + Visible = true + }; + notification.ShowBalloonTip(3000); + } +} +``` + +### Eye Strain Reduction with Blue Light Filtering + +Combine with Windows Night Light for comprehensive eye care: + +```powershell +# eye-care-scheduler.ps1 +# Automatically adjust monitors throughout the day for eye health + +function Set-EyeCareMode { + param([string]$Mode) + + switch ($Mode) { + "Morning" { + # Bright, energizing settings + ddcswitch set 0 brightness 85% + ddcswitch set 0 contrast 80% + # Enable Windows Night Light (warm colors) + reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount" /v "NightLightEnabled" /t REG_DWORD /d 0 /f + Write-Host "๐ŸŒ… Morning mode: Bright and energizing" -ForegroundColor Yellow + } + + "Midday" { + # Maximum brightness for productivity + ddcswitch set 0 brightness 90% + ddcswitch set 0 contrast 75% + Write-Host "โ˜€๏ธ Midday mode: Maximum productivity" -ForegroundColor Green + } + + "Evening" { + # Reduced brightness, warmer colors + ddcswitch set 0 brightness 60% + ddcswitch set 0 contrast 70% + # Enable Night Light + reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount" /v "NightLightEnabled" /t REG_DWORD /d 1 /f + Write-Host "๐ŸŒ† Evening mode: Reduced strain" -ForegroundColor Orange + } + + "Night" { + # Very dim for late work + ddcswitch set 0 brightness 25% + ddcswitch set 0 contrast 65% + Write-Host "๐ŸŒ™ Night mode: Minimal eye strain" -ForegroundColor Blue + } + } +} + +# Schedule throughout the day +$hour = (Get-Date).Hour + +if ($hour -ge 6 -and $hour -lt 10) { + Set-EyeCareMode "Morning" +} elseif ($hour -ge 10 -and $hour -lt 16) { + Set-EyeCareMode "Midday" +} elseif ($hour -ge 16 -and $hour -lt 20) { + Set-EyeCareMode "Evening" +} else { + Set-EyeCareMode "Night" +} +``` + +### Multi-Monitor Workspace Management + +Intelligent workspace switching based on active applications: + +```powershell +# workspace-manager.ps1 +# Automatically configure monitors based on active applications + +function Get-ActiveApplication { + Add-Type @" + using System; + using System.Runtime.InteropServices; + using System.Text; + public class Win32 { + [DllImport("user32.dll")] + public static extern IntPtr GetForegroundWindow(); + [DllImport("user32.dll")] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); + [DllImport("user32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); + } +"@ + + $hwnd = [Win32]::GetForegroundWindow() + $processId = 0 + [Win32]::GetWindowThreadProcessId($hwnd, [ref]$processId) + + return Get-Process -Id $processId -ErrorAction SilentlyContinue +} + +function Set-WorkspaceForApplication { + param([string]$ProcessName) + + $workspaceConfigs = @{ + "Code" = @{ + Description = "Visual Studio Code - Development" + Monitor0 = @{ Input = "DP1"; Brightness = 70; Contrast = 75 } + Monitor1 = @{ Input = "DP2"; Brightness = 65; Contrast = 75 } + } + "chrome" = @{ + Description = "Web Browsing" + Monitor0 = @{ Input = "DP1"; Brightness = 75; Contrast = 80 } + Monitor1 = @{ Input = "DP2"; Brightness = 70; Contrast = 80 } + } + "Photoshop" = @{ + Description = "Photo Editing - Color Accurate" + Monitor0 = @{ Input = "DP1"; Brightness = 80; Contrast = 85 } + Monitor1 = @{ Input = "DP2"; Brightness = 75; Contrast = 85 } + } + "Steam" = @{ + Description = "Gaming Mode" + Monitor0 = @{ Input = "HDMI1"; Brightness = 95; Contrast = 90 } + Monitor1 = @{ Input = "HDMI2"; Brightness = 90; Contrast = 85 } + } + "obs64" = @{ + Description = "Streaming Setup" + Monitor0 = @{ Input = "DP1"; Brightness = 85; Contrast = 80 } + Monitor1 = @{ Input = "HDMI1"; Brightness = 80; Contrast = 85 } + } + } + + if ($workspaceConfigs.ContainsKey($ProcessName)) { + $config = $workspaceConfigs[$ProcessName] + Write-Host "๐Ÿ–ฅ๏ธ Configuring workspace: $($config.Description)" -ForegroundColor Cyan + + # Apply to Monitor 0 + if ($config.Monitor0) { + ddcswitch set 0 $config.Monitor0.Input + ddcswitch set 0 brightness "$($config.Monitor0.Brightness)%" + ddcswitch set 0 contrast "$($config.Monitor0.Contrast)%" + } + + # Apply to Monitor 1 if exists + if ($config.Monitor1) { + $monitors = ddcswitch list --json | ConvertFrom-Json + if ($monitors.monitors.Count -gt 1) { + ddcswitch set 1 $config.Monitor1.Input + ddcswitch set 1 brightness "$($config.Monitor1.Brightness)%" + ddcswitch set 1 contrast "$($config.Monitor1.Contrast)%" + } + } + + return $true + } + + return $false +} + +# Monitor active application and adjust workspace +$lastProcess = "" +while ($true) { + $currentProcess = Get-ActiveApplication + + if ($currentProcess -and $currentProcess.ProcessName -ne $lastProcess) { + $configured = Set-WorkspaceForApplication $currentProcess.ProcessName + if ($configured) { + $lastProcess = $currentProcess.ProcessName + } + } + + Start-Sleep -Seconds 2 +} +``` + +### Meeting Room Display Management + +Automatically configure displays for presentations and video calls: + +```powershell +# meeting-room-controller.ps1 +# Integrate with Outlook calendar for automatic display management + +Add-Type -AssemblyName Microsoft.Office.Interop.Outlook + +function Get-UpcomingMeeting { + $outlook = New-Object -ComObject Outlook.Application + $namespace = $outlook.GetNamespace("MAPI") + $calendar = $namespace.GetDefaultFolder(9) # Calendar folder + + $now = Get-Date + $endTime = $now.AddHours(1) + + $filter = "[Start] >= '$($now.ToString("MM/dd/yyyy HH:mm"))' AND [Start] <= '$($endTime.ToString("MM/dd/yyyy HH:mm"))'" + $appointments = $calendar.Items.Restrict($filter) + + return $appointments | Select-Object -First 1 +} + +function Set-PresentationMode { + Write-Host "๐Ÿ“Š Activating Presentation Mode" -ForegroundColor Green + + # High brightness for projector visibility + ddcswitch set 0 brightness 100% + ddcswitch set 0 contrast 90% + + # Switch to presentation input (HDMI for projector) + ddcswitch set 0 HDMI1 + + # Duplicate display for presenter view + DisplaySwitch.exe /duplicate + + # Disable screen saver + powercfg /change standby-timeout-ac 0 + powercfg /change monitor-timeout-ac 0 +} + +function Set-VideoCallMode { + Write-Host "๐Ÿ“น Activating Video Call Mode" -ForegroundColor Blue + + # Optimal brightness for webcam lighting + ddcswitch set 0 brightness 85% + ddcswitch set 0 contrast 80% + + # PC input for video call software + ddcswitch set 0 DP1 + + # Extend displays for notes/chat + DisplaySwitch.exe /extend +} + +function Set-NormalMode { + Write-Host "๐Ÿ’ผ Returning to Normal Mode" -ForegroundColor Gray + + # Standard work brightness + ddcswitch set 0 brightness 70% + ddcswitch set 0 contrast 75% + + # Restore power settings + powercfg /change standby-timeout-ac 15 + powercfg /change monitor-timeout-ac 10 +} + +# Check for upcoming meetings +$meeting = Get-UpcomingMeeting + +if ($meeting) { + $timeUntilMeeting = ($meeting.Start - (Get-Date)).TotalMinutes + + if ($timeUntilMeeting -le 5 -and $timeUntilMeeting -gt 0) { + if ($meeting.Subject -match "presentation|demo|training") { + Set-PresentationMode + } elseif ($meeting.Subject -match "call|meeting|standup|sync") { + Set-VideoCallMode + } + + # Schedule return to normal mode after meeting + $meetingDuration = ($meeting.End - $meeting.Start).TotalMinutes + Start-Job -ScriptBlock { + param($Duration) + Start-Sleep -Seconds ($Duration * 60) + & "meeting-room-controller.ps1" -RestoreNormal + } -ArgumentList $meetingDuration + } +} +``` + +These creative integrations showcase ddcswitch's unique position in the Windows ecosystem, leveraging its JSON output and comprehensive VCP support for automation scenarios that weren't possible with traditional DDC/CI tools. + diff --git a/README.md b/README.md index b1cc3c2..8f7dbd9 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,23 @@ A Windows command-line utility to control monitor settings via DDC/CI (Display Data Channel Command Interface). Control input sources, brightness, contrast, and other VCP features without touching physical buttons. +The project is pre-configured with NativeAOT, which produces a native executable with instant startup and no .NET runtime dependency. + ๐Ÿ“š **[Examples](EXAMPLES.md)** | ๐Ÿ“ **[Changelog](CHANGELOG.md)** ## Features - ๐Ÿ–ฅ๏ธ **List all DDC/CI capable monitors** with their current input sources -- **Detailed EDID information** - View monitor specifications, capabilities, and color characteristics +- ๐Ÿ” **EDID information** - View monitor specifications, capabilities, and color characteristics - ๐Ÿ”„ **Switch monitor inputs** programmatically (HDMI, DisplayPort, DVI, VGA, etc.) - ๐Ÿ”† **Control brightness and contrast** with percentage values (0-100%) - ๐ŸŽ›๏ธ **Comprehensive VCP feature support** - Access all MCCS standardized monitor controls -- ๐Ÿท๏ธ **Feature categories and discovery** - Browse VCP features by category (Image, Color, Geometry, Audio, etc.) - ๐Ÿ” **VCP scanning** to discover all supported monitor features - ๐ŸŽฏ **Simple CLI interface** perfect for scripts, shortcuts, and hotkeys - ๐Ÿ“Š **JSON output support** - Machine-readable output for automation and integration - โšก **Fast and lightweight** - NativeAOT compiled for instant startup - ๐Ÿ“ฆ **True native executable** - No .NET runtime dependency required -- ๐ŸชŸ **Windows-only** - uses native Windows DDC/CI APIs (use ddcutil on Linux) +- ๐ŸชŸ **Windows-only** - uses native Windows DDC/CI APIs (use [ddcutil](https://www.ddcutil.com/) on Linux) ## Installation @@ -43,7 +44,16 @@ choco upgrade ddcswitch ### Pre-built Binary -Download the latest release from the [Releases](../../releases) page and extract `ddcswitch.exe` to a folder in your PATH. +Download the latest release from the [Releases](../../releases) page and extract `ddcswitch.exe` to a folder in your `PATH`. + +#### How to add to PATH: +1. Copy `ddcswitch.exe` to a folder (e.g., `C:\Tools\ddcswitch\`). +2. Open Start Menu, search "Environment Variables", and select "Edit the system environment variables" +3. Click "Environment Variables..." +4. Under "System variables", select "Path" and click "Edit..." +5. Click "New" and add the folder path (e.g., `C:\Tools\ddcswitch\`) +6. Click OK on all dialogs to apply changes. +7. Restart any open command prompts or PowerShell windows. ### Build from Source @@ -60,12 +70,13 @@ cd ddcswitch dotnet publish -c Release ``` -The project is pre-configured with NativeAOT (`true`), which produces a ~3-5 MB native executable with instant startup and no .NET runtime dependency. - Executable location: `ddcswitch/bin/Release/net10.0/win-x64/publish/ddcswitch.exe` ## Usage +[!NOTE] +JSON output is supported with `--json` flag all data and commands. + ### List Monitors Display all DDC/CI capable monitors with their current input sources: @@ -74,36 +85,6 @@ Display all DDC/CI capable monitors with their current input sources: ddcswitch list ``` -Example output: -``` -โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -โ”‚ Index โ”‚ Monitor Name โ”‚ Device โ”‚ Current Input โ”‚ Status โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ 0 โ”‚ Generic PnP Monitor โ”‚ \\.\DISPLAY2 โ”‚ HDMI1 (0x11) โ”‚ OK โ”‚ -โ”‚ 1* โ”‚ VG270U P โ”‚ \\.\DISPLAY1 โ”‚ DisplayPort1 (DP1) (0x0F) โ”‚ OK โ”‚ -โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ -``` - -#### Verbose Listing - -Add `--verbose` to include brightness and contrast information: - -```powershell -ddcswitch list --verbose -``` - -Example output: -``` -โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -โ”‚ Index โ”‚ Monitor Name โ”‚ Device โ”‚ Current Input โ”‚ Status โ”‚ Brightness โ”‚ Contrast โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ 0 โ”‚ Generic PnP Monitor โ”‚ \\.\DISPLAY2 โ”‚ HDMI1 (0x11) โ”‚ OK โ”‚ 75% โ”‚ 80% โ”‚ -โ”‚ 1* โ”‚ VG270U P โ”‚ \\.\DISPLAY1 โ”‚ DisplayPort1 (DP1) (0x0F) โ”‚ OK โ”‚ N/A โ”‚ N/A โ”‚ -โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ -``` - -Add `--json` for machine-readable output (see [EXAMPLES.md](EXAMPLES.md) for automation examples). - ### Monitor Information (EDID) View detailed EDID (Extended Display Identification Data) information for a specific monitor: @@ -112,72 +93,33 @@ View detailed EDID (Extended Display Identification Data) information for a spec ddcswitch info 0 ``` -The info command provides comprehensive monitor details including: -- **EDID version** and manufacturer information -- **Model name**, serial number, and manufacture date -- **Video input type** (Digital/Analog) -- **Supported features** (DPMS power modes, display type, color space) -- **Chromaticity coordinates** for color calibration (red, green, blue, white points in CIE 1931 color space) -- **Current input source** status - -JSON output is supported with `--json` flag for programmatic access to all EDID data: - -```powershell -ddcswitch info 0 --json -``` - ### Get Current Settings -Get all VCP features for a specific monitor: - -```powershell -ddcswitch get 0 -``` - -This will scan and display all supported VCP features for monitor 0, showing their names, access types, current values, and maximum values. - -You can also use the monitor name instead of the index (partial name matching supported): +Get a specific feature by monitor index or name: ```powershell -# Get all settings by monitor name -ddcswitch get "VG270U P" -ddcswitch get "Generic PnP" -``` - -Get a specific feature: - -```powershell -# Get current input source -ddcswitch get 0 input - -# Get brightness as percentage +# Get brightness by monitor index ddcswitch get 0 brightness -# Get contrast as percentage -ddcswitch get 0 contrast - -# Works with monitor names too -ddcswitch get "VG270U P" brightness -ddcswitch get "Generic PnP" input +# Get input source by monitor name (partial match supported) +ddcswitch get "VG270U" input ``` -Output: `Monitor: Generic PnP Monitor` / `Brightness: 75% (120/160)` - ### Set Monitor Settings -Switch a monitor to a different input: +Set brightness, contrast, or switch inputs by monitor index or name: ```powershell -# By monitor index -ddcswitch set 0 HDMI1 +# Set brightness by index +ddcswitch set 0 brightness 75% -# By monitor name (partial match) +# Switch input by monitor name (partial match supported) ddcswitch set "LG ULTRAGEAR" HDMI2 ``` ### Toggle Between Input Sources -Automatically switch between two input sources without specifying which one: +Automatically switch between two input sources: ```powershell # Toggle between HDMI1 and DisplayPort1 @@ -187,12 +129,7 @@ ddcswitch toggle 0 HDMI1 DP1 ddcswitch toggle "LG ULTRAGEAR" HDMI1 HDMI2 ``` -The toggle command detects the current input and switches to the alternate one: -- If current input is HDMI1 โ†’ switches to DP1 -- If current input is DP1 โ†’ switches to HDMI1 -- If current input is neither โ†’ switches to HDMI1 (with warning) - -Perfect for hotkeys and automation where you want to switch between two specific inputs without knowing which one is currently active. +The toggle command detects the current input and switches to the alternate one - perfect for hotkeys and automation. ### Raw VCP Access @@ -226,33 +163,6 @@ ddcswitch get 0 ddcswitch get "VG270U" ``` -### VCP Feature Categories and Discovery - -Discover and browse VCP features by category: - -```powershell -# List all available categories -ddcswitch list --categories - -# List features in a specific category -ddcswitch list --category image -ddcswitch list --category color -ddcswitch list --category audio -``` - -Example output: -``` -Image Adjustment Features: -- brightness (0x10): Brightness control -- contrast (0x12): Contrast control -- sharpness (0x87): Sharpness control -- backlight (0x13): Backlight control - -Color Control Features: -- red-gain (0x16): Video gain: Red -- green-gain (0x18): Video gain: Green -- blue-gain (0x1A): Video gain: Blue -``` ### Supported Features @@ -273,102 +183,55 @@ Color Control Features: - **Geometry**: `h-position`, `v-position`, `clock`, `phase` (mainly for CRT monitors) - **Presets**: `restore-defaults`, `degauss` (VCP 0x04, 0x01) -#### VCP Feature Categories -- **Image Adjustment**: brightness, contrast, sharpness, backlight, etc. -- **Color Control**: RGB gains, color temperature, gamma, hue, saturation -- **Geometry**: position, size, pincushion controls (mainly CRT) -- **Audio**: volume, mute, balance, treble, bass -- **Preset**: factory defaults, degauss, calibration -- **Miscellaneous**: power mode, OSD settings, firmware info #### Raw VCP Codes - Any VCP code from `0x00` to `0xFF` - Values must be within the monitor's supported range - Use hex format: `0x10`, `0x12`, etc. -## Use Cases - -### Quick Examples +## Quick Start -**Switch multiple monitors:** -```powershell -ddcswitch set 0 HDMI1 -ddcswitch set 1 DP1 -``` +### Basic Usage Examples -**Toggle between input sources:** ```powershell -# Toggle main monitor between HDMI1 and DisplayPort -ddcswitch toggle 0 HDMI1 DP1 +# List monitors +ddcswitch list -# Toggle secondary monitor between HDMI inputs -ddcswitch toggle 1 HDMI1 HDMI2 -``` +# Switch monitor input +ddcswitch set 0 HDMI1 -**Control comprehensive VCP features:** -```powershell +# Adjust brightness ddcswitch set 0 brightness 75% -ddcswitch set 0 contrast 80% -ddcswitch get 0 brightness - -# Color controls -ddcswitch set 0 red-gain 90% -ddcswitch set 0 green-gain 85% -ddcswitch set 0 blue-gain 95% - -# Audio controls (if supported) -ddcswitch set 0 volume 50% -ddcswitch set 0 mute 1 ``` -**VCP feature discovery:** -```powershell -# List all available VCP feature categories -ddcswitch list --categories +### JSON Output -# List features in a specific category -ddcswitch list --category color +All commands support `--json` for machine-readable output, perfect for automation: -# Search for features by name -ddcswitch get 0 bright # Matches "brightness" - -# Or by monitor name -ddcswitch get "VG270U" bright -``` - -**Desktop shortcut:** -Create a shortcut with target: `C:\Path\To\ddcswitch.exe set 0 brightness 50%` +```powershell +# Get monitor list as JSON +ddcswitch list --json -**AutoHotkey:** -```autohotkey -^!h::Run, ddcswitch.exe set 0 HDMI1 ; Ctrl+Alt+H for HDMI1 -^!d::Run, ddcswitch.exe set 0 DP1 ; Ctrl+Alt+D for DisplayPort -^!b::Run, ddcswitch.exe set 0 brightness 75% ; Ctrl+Alt+B for 75% brightness +# Get specific monitor info as JSON +ddcswitch info 0 --json ``` -### JSON Output for Automation +### Plain Text Output -All commands support `--json` for machine-readable output: +To disable colors and icons (for logging or automation), set the `NO_COLOR` environment variable: ```powershell -# PowerShell: Conditional switching -$result = ddcswitch get 0 --json | ConvertFrom-Json -if ($result.currentInputCode -ne "0x11") { - ddcswitch set 0 HDMI1 -} +$env:NO_COLOR = "1" +ddcswitch list ``` -```python -# Python: Switch all monitors -import subprocess, json -data = json.loads(subprocess.run(['ddcswitch', 'list', '--json'], - capture_output=True, text=True).stdout) -for m in data['monitors']: - if m['status'] == 'ok': - subprocess.run(['ddcswitch', 'set', str(m['index']), 'HDMI1']) -``` +### Windows Shortcuts + +Create a desktop shortcut to quickly adjust settings: -๐Ÿ“š **See [EXAMPLES.md](EXAMPLES.md) for comprehensive automation examples** including Stream Deck, Task Scheduler, Python, Node.js, Rust, and more. +**Target:** `C:\Path\To\ddcswitch.exe set 0 HDMI1` + +๐Ÿ“š **For more examples** including hotkeys, automation scripts, Stream Deck integration, and advanced usage, see **[EXAMPLES.md](EXAMPLES.md)**. ## Troubleshooting @@ -389,31 +252,17 @@ for m in data['monitors']: - DDC/CI can be slow - wait a few seconds between commands - Some monitors need to be on the target input at least once before DDC/CI can switch to it - Check monitor OSD settings for DDC/CI enable/disable options +- Power cycle the monitor and/or remove and reconnect the video cable ### Current input displays incorrectly Some monitors have non-standard DDC/CI implementations and may report incorrect current input values, even though input switching still works correctly. This is a monitor firmware limitation, not a tool issue. -If you need to verify DDC/CI values or troubleshoot monitor-specific issues, try [ControlMyMonitor](https://www.nirsoft.net/utils/control_my_monitor.html) by NirSoft - a comprehensive GUI tool for DDC/CI debugging. - -## Technical Details - -ddcswitch uses the Windows DXVA2 API to communicate with monitors via DDC/CI protocol. It reads/writes VCP (Virtual Control Panel) features following the MCCS specification. - -**Common VCP Codes:** -- `0x10` Brightness, `0x12` Contrast, `0x60` Input Source -- `0x01` VGA, `0x03` DVI, `0x0F` DisplayPort 1, `0x10` DisplayPort 2, `0x11` HDMI 1, `0x12` HDMI 2 - -**VCP Feature Types:** -- **Read-Write**: Can get and set values (brightness, contrast, input) -- **Read-Only**: Can only read current value (some monitor info) -- **Write-Only**: Can only set values (some calibration features) - -**NativeAOT Compatible:** Uses source generators for JSON, `DllImport` for P/Invoke, and zero reflection for reliable AOT compilation. +If you prefer a graphical interface over the command-line, try [ControlMyMonitor](https://www.nirsoft.net/utils/control_my_monitor.html) by NirSoft - a comprehensive GUI tool for DDC/CI control and debugging. ## Why Windows Only? -Linux has excellent DDC/CI support through `ddcutil`, which is more mature and feature-rich. This tool focuses on Windows where native CLI options are limited. +Linux has excellent DDC/CI support through `ddcutil`, which is more mature and feature-rich. Windows needed a similar command-line tool - while `winddcutil` exists, it requires Python dependencies. This project provides a standalone native executable with no runtime requirements, though it's not trying to be a direct clone of the Linux ddcutil. ## Contributing @@ -423,6 +272,16 @@ Contributions welcome! Please open issues for bugs or feature requests. MIT License - see LICENSE file for details +## Disclaimer and Warranty + +**NO WARRANTY**: This software is provided "AS IS" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability, fitness for a particular purpose, or non-infringement. The entire risk as to the quality and performance of the software is with you. + +**LIMITATION OF LIABILITY**: In no event shall the authors, copyright holders, or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. + +**MONITOR DAMAGE**: The authors and contributors of this software are not responsible for any damage to monitors, display devices, or other hardware that may result from the use of this software. DDC/CI commands can potentially affect monitor settings in ways that may cause temporary or permanent changes to display behavior. Users assume all risk when using this software to control monitor settings. + +**USE AT YOUR OWN RISK**: By using this software, you acknowledge that you understand the risks involved in sending DDC/CI commands to your monitors and that you use this software entirely at your own risk. It is recommended to test commands carefully and ensure you can restore your monitor settings manually if needed. + ## Acknowledgments - Inspired by [ddcutil](https://www.ddcutil.com) for Linux diff --git a/build.cmd b/build.cmd deleted file mode 100644 index 2b507cc..0000000 --- a/build.cmd +++ /dev/null @@ -1,53 +0,0 @@ -๏ปฟ@echo off -setlocal enabledelayedexpansion - -echo ======================================== -echo Building ddcswitch with NativeAOT -echo ======================================== -echo. - -REM Clean previous build -echo Cleaning previous build... -dotnet clean DDCSwitch\DDCSwitch.csproj -c Release -if errorlevel 1 ( - echo ERROR: Clean failed - exit /b 1 -) -echo. - -REM Build with NativeAOT -echo Building with NativeAOT... -dotnet publish DDCSwitch\DDCSwitch.csproj -c Release -r win-x64 --self-contained -if errorlevel 1 ( - echo ERROR: Build failed - exit /b 1 -) -echo. - -REM Create dist folder -echo Creating dist folder... -if not exist "dist" mkdir "dist" - -REM Copy the NativeAOT executable -echo Copying executable to dist folder... -copy /Y "DDCSwitch\bin\Release\net10.0\win-x64\publish\ddcswitch.exe" "dist\ddcswitch.exe" -if errorlevel 1 ( - echo ERROR: Failed to copy executable - exit /b 1 -) - -echo. -echo ======================================== -echo Build completed successfully! -echo Output: dist\ddcswitch.exe -echo ======================================== - -REM Display file size -for %%A in ("dist\ddcswitch.exe") do ( - set size=%%~zA - set /a sizeMB=!size! / 1048576 - echo File size: !sizeMB! MB -) - -endlocal - diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..ae4d5f1 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,47 @@ +Write-Host "========================================" +Write-Host "Building ddcswitch with NativeAOT" +Write-Host "========================================" +Write-Host "" + +# Clean previous build +Write-Host "Cleaning previous build..." +dotnet clean DDCSwitch\DDCSwitch.csproj -c Release +if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Clean failed" -ForegroundColor Red + exit 1 +} +Write-Host "" + +# Build with NativeAOT +Write-Host "Building with NativeAOT..." +dotnet publish DDCSwitch\DDCSwitch.csproj -c Release -r win-x64 --self-contained +if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Build failed" -ForegroundColor Red + exit 1 +} +Write-Host "" + +# Create dist folder +Write-Host "Creating dist folder..." +if (-not (Test-Path "dist")) { + New-Item -ItemType Directory -Path "dist" | Out-Null +} + +# Copy the NativeAOT executable +Write-Host "Copying executable to dist folder..." +Copy-Item -Path "DDCSwitch\bin\Release\net10.0\win-x64\publish\ddcswitch.exe" -Destination "dist\ddcswitch.exe" -Force +if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to copy executable" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "========================================" +Write-Host "Build completed successfully!" +Write-Host "Output: dist\ddcswitch.exe" +Write-Host "========================================" + +# Display file size +$fileInfo = Get-Item "dist\ddcswitch.exe" +$sizeMB = [math]::Round($fileInfo.Length / 1MB, 2) +Write-Host "File size: $sizeMB MB" diff --git a/tools/GenerateVcpFeatures.py b/tools/GenerateVcpFeatures.py index f2c5355..bce808d 100644 --- a/tools/GenerateVcpFeatures.py +++ b/tools/GenerateVcpFeatures.py @@ -54,7 +54,7 @@ def generate_all_features_list(features): def main(): script_dir = Path(__file__).parent json_file = script_dir.parent / 'DDCSwitch' / 'VcpFeatureData.json' - output_file = script_dir.parent / 'DDCSwitch' / 'VcpFeature.Generated.cs' + output_file = script_dir.parent / 'DDCSwitch' / 'VcpFeatures' / 'VcpFeature.Generated.cs' print(f"Loading {json_file}...") with open(json_file, 'r', encoding='utf-8') as f: