From 12d898d7fdbc2cab9ef54a7e42cab9ccb185836e Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:13:26 -0600 Subject: [PATCH 01/13] Add toggle command to switch between two input sources; update documentation and examples --- CHANGELOG.md | 5 + DDCSwitch/Commands/CommandRouter.cs | 1 + DDCSwitch/Commands/HelpCommand.cs | 5 + DDCSwitch/Commands/ToggleCommand.cs | 401 ++++++++++++++++++++++++++++ DDCSwitch/JsonContext.cs | 10 + EXAMPLES.md | 58 ++++ README.md | 28 ++ 7 files changed, 508 insertions(+) create mode 100644 DDCSwitch/Commands/ToggleCommand.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 392330f..ebb9193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to ddcswitch will be documented in this file. +## [1.0.3] - 2026-01-08 + +### Added +- Added the toggle command to flip a monitor between two input sources + ## [1.0.2] - 2026-01-07 ### Added diff --git a/DDCSwitch/Commands/CommandRouter.cs b/DDCSwitch/Commands/CommandRouter.cs index e9761c3..0cefadb 100644 --- a/DDCSwitch/Commands/CommandRouter.cs +++ b/DDCSwitch/Commands/CommandRouter.cs @@ -37,6 +37,7 @@ public static int Route(string[] args) "list" or "ls" => ListCommand.Execute(jsonOutput, verboseOutput), "get" => GetCommand.Execute(filteredArgs, jsonOutput), "set" => SetCommand.Execute(filteredArgs, jsonOutput), + "toggle" => ToggleCommand.Execute(filteredArgs, jsonOutput), "version" or "-v" or "--version" => HelpCommand.ShowVersion(jsonOutput), "help" or "-h" or "--help" or "/?" => HelpCommand.ShowUsage(), _ => InvalidCommand(filteredArgs[0], jsonOutput) diff --git a/DDCSwitch/Commands/HelpCommand.cs b/DDCSwitch/Commands/HelpCommand.cs index 54895b0..5b2697c 100644 --- a/DDCSwitch/Commands/HelpCommand.cs +++ b/DDCSwitch/Commands/HelpCommand.cs @@ -58,6 +58,9 @@ public static int ShowUsage() commandsTable.AddRow( "[cyan]set[/] [green][/] [blue][/] [magenta][/]", "Set value for a monitor feature"); + commandsTable.AddRow( + "[cyan]toggle[/] [green][/] [blue][/] [blue][/]", + "Toggle between two input sources automatically"); commandsTable.AddRow( "[cyan]version[/] [dim]or[/] [cyan]-v[/]", "Display version information"); @@ -90,6 +93,8 @@ public static int ShowUsage() AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch get 0 brightness"); AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch set 0 input HDMI1"); AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch set 1 brightness 75%"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch toggle 0 HDMI1 DP1"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch toggle \"Dell Monitor\" HDMI1 HDMI2"); return 0; } diff --git a/DDCSwitch/Commands/ToggleCommand.cs b/DDCSwitch/Commands/ToggleCommand.cs new file mode 100644 index 0000000..e3dc3d3 --- /dev/null +++ b/DDCSwitch/Commands/ToggleCommand.cs @@ -0,0 +1,401 @@ +using Spectre.Console; +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class ToggleCommand +{ + public static int Execute(string[] args, bool jsonOutput) + { + // Require exactly 4 arguments: toggle + if (args.Length < 4) + { + return HandleMissingArguments(args.Length, jsonOutput); + } + + // Extract arguments + string monitorArg = args[1]; + string input1Arg = args[2]; + string input2Arg = args[3]; + + // Validate input sources and get their values + if (!InputSource.TryParse(input1Arg, out uint input1Value)) + { + return HandleInvalidInput(input1Arg, "first", jsonOutput); + } + + if (!InputSource.TryParse(input2Arg, out uint input2Value)) + { + return HandleInvalidInput(input2Arg, "second", jsonOutput); + } + + // Validate that inputs are different + if (input1Value == input2Value) + { + return HandleIdenticalInputs(input1Arg, input2Arg, jsonOutput); + } + + // Enumerate monitors + var monitors = MonitorController.EnumerateMonitors(); + + if (monitors.Count == 0) + { + return HandleNoMonitors(jsonOutput); + } + + // Find the specified monitor + var monitor = MonitorController.FindMonitor(monitors, monitorArg); + + if (monitor == null) + { + return HandleMonitorNotFound(monitors, monitorArg, jsonOutput); + } + + // Execute the toggle operation + int result = ExecuteToggle(monitor, input1Value, input2Value, input1Arg, input2Arg, jsonOutput); + + // Cleanup resources + foreach (var m in monitors) + { + m.Dispose(); + } + + return result; + } + + private static int HandleMissingArguments(int actualCount, bool jsonOutput) + { + string errorMessage = actualCount switch + { + 0 => "Command 'toggle' requires monitor and two input sources", + 1 => "Monitor and two input sources required", + 2 => "Two input sources required", + 3 => "Second input source required", + _ => "Invalid number of arguments" + }; + + if (jsonOutput) + { + var error = new ErrorResponse(false, errorMessage); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine("Usage: [yellow]ddcswitch toggle [/]"); + } + + return 1; + } + + + + private static int HandleInvalidInput(string invalidInput, string position, bool jsonOutput) + { + string errorMessage = $"Invalid {position} input source: '{invalidInput}'"; + string suggestion = "Valid input sources include: HDMI1, HDMI2, DisplayPort1, DisplayPort2, DVI1, DVI2, VGA1, VGA2, or numeric codes (e.g., 0x11, 17)"; + + if (jsonOutput) + { + var error = new ErrorResponse(false, $"{errorMessage}. {suggestion}"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine($"[dim]{suggestion}[/]"); + } + + return 1; + } + + private static int HandleIdenticalInputs(string input1, string input2, bool jsonOutput) + { + string errorMessage = $"Input sources must be different. Both inputs resolve to the same source: '{input1}' and '{input2}'"; + string suggestion = "Please specify two different input sources to toggle between"; + + if (jsonOutput) + { + var error = new ErrorResponse(false, $"{errorMessage}. {suggestion}"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine($"[dim]{suggestion}[/]"); + } + + return 1; + } + + /// + /// Detects the current input source from the monitor using VCP code 0x60 + /// + /// Monitor to read current input from + /// Whether to format output as JSON + /// The detected current input value + /// 0 on success, 1 on failure + private static int DetectCurrentInput(Monitor monitor, bool jsonOutput, out uint currentInput) + { + currentInput = 0; + + // Try to read current input source using VCP code 0x60 + if (monitor.TryGetVcpFeature(InputSource.VcpInputSource, out currentInput, out uint maxValue, out int errorCode)) + { + // Successfully detected current input + return 0; + } + + // Handle detection failure + return HandleInputDetectionFailure(monitor, errorCode, jsonOutput); + } + + private static int HandleInputDetectionFailure(Monitor monitor, int errorCode, bool jsonOutput) + { + string errorMessage = errorCode switch + { + 0x00000006 => "Monitor handle is invalid", // ERROR_INVALID_HANDLE + 0x00000057 => "Monitor does not support input source reading", // ERROR_INVALID_PARAMETER + 0x00000102 => "Monitor communication timeout", // WAIT_TIMEOUT + _ => $"Failed to read current input source (Error: 0x{errorCode:X8})" + }; + + string fallbackMessage = "Will use fallback behavior and switch to first input source"; + string fullMessage = $"{errorMessage}. {fallbackMessage}"; + + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var error = new ErrorResponse(false, fullMessage, monitorRef); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + string escapedMonitorName = monitor.Name.Replace("[", "[[").Replace("]", "]]"); + ConsoleOutputFormatter.WriteError($"Monitor [[{monitor.Index}]] {escapedMonitorName}: {errorMessage}"); + AnsiConsole.MarkupLine($"[yellow]Warning:[/] {fallbackMessage}"); + } + + // Return 2 to indicate a warning condition that allows fallback behavior + // This is different from return 1 which indicates a hard error + return 2; + } + + /// + /// Determines which input to switch to based on current input and the two specified inputs + /// + /// The currently active input source + /// First input source value + /// Second input source value + /// The target input value to switch to + private static uint DetermineTargetInput(uint currentInput, uint input1Value, uint input2Value) + { + // If current input matches input1, switch to input2 + if (currentInput == input1Value) + { + return input2Value; + } + + // If current input matches input2, switch to input1 + if (currentInput == input2Value) + { + return input1Value; + } + + // If current input is neither specified input, default to input1 + // This handles the edge case where the current input is something else entirely + return input1Value; + } + + private static int HandleNoMonitors(bool jsonOutput) + { + string errorMessage = "No DDC/CI capable monitors found"; + + if (jsonOutput) + { + var error = new ErrorResponse(false, errorMessage); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + } + + return 1; + } + + private static int HandleMonitorNotFound(List monitors, string monitorId, bool jsonOutput) + { + string errorMessage = $"Monitor '{monitorId}' not found"; + + if (jsonOutput) + { + var error = new ErrorResponse(false, errorMessage); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine("Use [yellow]ddcswitch list[/] to see available monitors."); + } + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return 1; + } + + private static int ExecuteToggle(Monitor monitor, uint input1Value, uint input2Value, string input1Arg, string input2Arg, bool jsonOutput) + { + // Detect current input + int detectionResult = DetectCurrentInput(monitor, jsonOutput, out uint currentInput); + bool hasWarning = false; + string? warningMessage = null; + + // Handle detection failure with fallback behavior + if (detectionResult == 2) // Warning condition - detection failed but we can continue + { + hasWarning = true; + warningMessage = "Could not detect current input source, switching to first input source"; + currentInput = 0; // Will cause DetermineTargetInput to default to input1 + } + else if (detectionResult == 1) // Hard error + { + return 1; + } + + // Determine target input + uint targetInput = DetermineTargetInput(currentInput, input1Value, input2Value); + + // Check if current input is neither of the specified inputs + if (detectionResult == 0 && currentInput != input1Value && currentInput != input2Value) + { + hasWarning = true; + warningMessage = $"Current input '{InputSource.GetName(currentInput)}' is not one of the specified inputs, switching to '{InputSource.GetName(targetInput)}'"; + } + + // Perform the input switch + return PerformInputSwitch(monitor, currentInput, targetInput, input1Arg, input2Arg, hasWarning, warningMessage, jsonOutput); + } + + private static int PerformInputSwitch(Monitor monitor, uint currentInput, uint targetInput, string input1Arg, string input2Arg, bool hasWarning, string? warningMessage, bool jsonOutput) + { + bool success = false; + string? errorMessage = null; + + if (!jsonOutput) + { + string targetName = InputSource.GetName(targetInput); + AnsiConsole.Status() + .Start($"Switching {monitor.Name} to {targetName}...", ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("cyan")); + + if (!monitor.TrySetVcpFeature(InputSource.VcpInputSource, targetInput, out int errorCode)) + { + errorMessage = GetInputSwitchErrorMessage(monitor, targetInput, errorCode); + } + else + { + success = true; + } + + if (success) + { + // Give the monitor a moment to switch inputs + Thread.Sleep(1000); + } + }); + } + else + { + if (!monitor.TrySetVcpFeature(InputSource.VcpInputSource, targetInput, out int errorCode)) + { + errorMessage = GetInputSwitchErrorMessage(monitor, targetInput, errorCode); + } + else + { + success = true; + Thread.Sleep(1000); + } + } + + if (!success) + { + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var error = new ErrorResponse(false, errorMessage!, monitorRef); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage!); + } + + return 1; + } + + // Output success + OutputToggleSuccess(monitor, currentInput, targetInput, hasWarning, warningMessage, jsonOutput); + return 0; + } + + private static string GetInputSwitchErrorMessage(Monitor monitor, uint targetInput, int errorCode) + { + string targetName = InputSource.GetName(targetInput); + string escapedMonitorName = monitor.Name.Replace("[", "[[").Replace("]", "]]"); + + return errorCode switch + { + 0x00000006 => $"Monitor '{escapedMonitorName}' handle is invalid - DDC/CI communication failed", + 0x00000057 => $"Monitor '{escapedMonitorName}' does not support input source switching", + 0x00000102 => $"Timeout switching monitor '{escapedMonitorName}' to {targetName} - monitor may be unresponsive", + _ => $"Failed to switch monitor '{escapedMonitorName}' to {targetName} (Error: 0x{errorCode:X8})" + }; + } + + private static void OutputToggleSuccess(Monitor monitor, uint currentInput, uint targetInput, bool hasWarning, string? warningMessage, bool jsonOutput) + { + string fromInputName = InputSource.GetName(currentInput); + string toInputName = InputSource.GetName(targetInput); + + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var result = new ToggleInputResponse( + Success: true, + Monitor: monitorRef, + FromInput: fromInputName, + ToInput: toInputName, + FromInputCode: currentInput, + ToInputCode: targetInput, + Warning: hasWarning ? warningMessage : null + ); + Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.ToggleInputResponse)); + } + else + { + var successPanel = new Panel( + $"[bold cyan]Monitor:[/] {monitor.Name}\n" + + $"[bold yellow]From:[/] {fromInputName}\n" + + $"[bold green]To:[/] {toInputName}") + { + Header = new PanelHeader("[bold green]>> Input Toggled Successfully[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Green) + }; + + AnsiConsole.Write(successPanel); + + if (hasWarning && warningMessage != null) + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] {warningMessage}"); + } + } + } +} \ No newline at end of file diff --git a/DDCSwitch/JsonContext.cs b/DDCSwitch/JsonContext.cs index 059cdf0..02a5a5c 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -7,6 +7,7 @@ namespace DDCSwitch; [JsonSerializable(typeof(MonitorInfo))] [JsonSerializable(typeof(GetVcpResponse))] [JsonSerializable(typeof(SetVcpResponse))] +[JsonSerializable(typeof(ToggleInputResponse))] [JsonSerializable(typeof(VcpScanResponse))] [JsonSerializable(typeof(VcpFeatureInfo))] [JsonSerializable(typeof(VcpFeatureType))] @@ -25,6 +26,15 @@ internal record ErrorResponse(bool Success, string Error, MonitorReference? Moni internal record ListMonitorsResponse(bool Success, List? Monitors = null, string? Error = null); internal record GetVcpResponse(bool Success, MonitorReference Monitor, string FeatureName, uint RawValue, uint MaxValue, uint? PercentageValue = null, string? ErrorMessage = null); internal record SetVcpResponse(bool Success, MonitorReference Monitor, string FeatureName, uint SetValue, uint? PercentageValue = null, string? ErrorMessage = null); +internal record ToggleInputResponse( + bool Success, + MonitorReference Monitor, + string FromInput, + string ToInput, + uint FromInputCode, + uint ToInputCode, + string? Warning = null, + string? ErrorMessage = null); internal record VcpScanResponse(bool Success, MonitorReference Monitor, List Features, string? ErrorMessage = null); // Data models diff --git a/EXAMPLES.md b/EXAMPLES.md index 1873c3d..5c4c8b8 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -261,6 +261,64 @@ ddcswitch set 0 contrast 80% ddcswitch set 0 0x10 120 # Brightness (raw value) ``` +### Toggle Between Input Sources + +The toggle command automatically switches between two specified input sources by detecting the current input and switching to the alternate one: + +```powershell +# Toggle between HDMI1 and DisplayPort1 +ddcswitch toggle 0 HDMI1 DP1 + +# Toggle between HDMI1 and HDMI2 by monitor name +ddcswitch toggle "LG ULTRAGEAR" HDMI1 HDMI2 + +# Toggle with JSON output for automation +ddcswitch toggle 0 HDMI1 DP1 --json +``` + +#### Toggle Command Behavior + +- **Current input is HDMI1** → Switches to DP1 +- **Current input is DP1** → Switches to HDMI1 +- **Current input is neither** → Switches to HDMI1 (first input) with warning + +#### Toggle Examples + +```powershell +# Create toggle shortcuts for different monitor setups +# toggle-main-monitor.ps1 +ddcswitch toggle 0 HDMI1 DP1 +Write-Host "Toggled main monitor input" -ForegroundColor Green + +# toggle-secondary-monitor.ps1 +ddcswitch toggle 1 HDMI2 DP2 +Write-Host "Toggled secondary monitor input" -ForegroundColor Green + +# Smart toggle with status feedback +$result = ddcswitch toggle 0 HDMI1 DP1 --json | ConvertFrom-Json +if ($result.success) { + Write-Host "Switched from $($result.fromInput) to $($result.toInput)" -ForegroundColor Green +} else { + Write-Host "Toggle failed: $($result.errorMessage)" -ForegroundColor Red +} +``` + +#### AutoHotkey Toggle Integration + +```autohotkey +; Ctrl+Alt+T: Toggle between HDMI1 and DisplayPort +^!t:: + Run, ddcswitch.exe toggle 0 HDMI1 DP1, , Hide + TrayTip, ddcswitch, Input toggled, 1 + return + +; Ctrl+Alt+Shift+T: Toggle secondary monitor +^!+t:: + Run, ddcswitch.exe toggle 1 HDMI2 DP2, , Hide + TrayTip, ddcswitch, Secondary monitor toggled, 1 + return +``` + ## Brightness and Contrast Control ### Basic Brightness Control diff --git a/README.md b/README.md index 67844e0..695fe61 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,25 @@ ddcswitch set 0 HDMI1 ddcswitch set "LG ULTRAGEAR" HDMI2 ``` +### Toggle Between Input Sources + +Automatically switch between two input sources without specifying which one: + +```powershell +# Toggle between HDMI1 and DisplayPort1 +ddcswitch toggle 0 HDMI1 DP1 + +# Toggle by monitor name +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. + Set brightness or contrast with percentage values: ```powershell @@ -252,6 +271,15 @@ ddcswitch set 0 HDMI1 ddcswitch set 1 DP1 ``` +**Toggle between input sources:** +```powershell +# Toggle main monitor between HDMI1 and DisplayPort +ddcswitch toggle 0 HDMI1 DP1 + +# Toggle secondary monitor between HDMI inputs +ddcswitch toggle 1 HDMI1 HDMI2 +``` + **Control comprehensive VCP features:** ```powershell ddcswitch set 0 brightness 75% From 3d2c48706ac4e52a551a26adcfab3acb44236b97 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:29:23 -0600 Subject: [PATCH 02/13] Add Chocolatey installation instructions to README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 695fe61..53ed458 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,20 @@ A Windows command-line utility to control monitor settings via DDC/CI (Display D ## Installation +### Chocolatey (Recommended) + +Install via [Chocolatey](https://chocolatey.org/) package manager: + +```powershell +choco install ddcswitch +``` + +To upgrade to the latest version: + +```powershell +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. From 0ff4ae9cbfbb2442c34923997921484573c18485 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:40:24 -0600 Subject: [PATCH 03/13] Add EDID parsing functionality and update monitor data structure --- DDCSwitch/Commands/ListCommand.cs | 37 +++- DDCSwitch/DDCSwitch.csproj | 1 + DDCSwitch/EdidParser.cs | 307 ++++++++++++++++++++++++++++++ DDCSwitch/JsonContext.cs | 9 +- DDCSwitch/Monitor.cs | 36 ++++ DDCSwitch/MonitorController.cs | 4 + DDCSwitch/NativeMethods.cs | 71 ++++++- LICENSE | 2 +- 8 files changed, 461 insertions(+), 6 deletions(-) create mode 100644 DDCSwitch/EdidParser.cs diff --git a/DDCSwitch/Commands/ListCommand.cs b/DDCSwitch/Commands/ListCommand.cs index 87cb204..f00371c 100644 --- a/DDCSwitch/Commands/ListCommand.cs +++ b/DDCSwitch/Commands/ListCommand.cs @@ -125,7 +125,14 @@ private static void OutputJsonList(List monitors, bool verboseOutput) inputCode != null ? $"0x{inputCode:X2}" : null, status, brightness, - contrast); + contrast, + monitor.ManufacturerId, + monitor.ManufacturerName, + monitor.ModelName, + monitor.SerialNumber, + monitor.ProductCode, + monitor.ManufactureYear, + monitor.ManufactureWeek); }).ToList(); var result = new ListMonitorsResponse(true, monitorList); @@ -142,9 +149,13 @@ private static void OutputTableList(List monitors, bool verboseOutput) .AddColumn(new TableColumn("[bold yellow]Device[/]").LeftAligned()) .AddColumn(new TableColumn("[bold yellow]Current Input[/]").LeftAligned()); - // Add brightness and contrast columns if verbose mode is enabled + // Add EDID and feature columns if verbose mode is enabled if (verboseOutput) { + table.AddColumn(new TableColumn("[bold yellow]Manufacturer[/]").LeftAligned()); + table.AddColumn(new TableColumn("[bold yellow]Model[/]").LeftAligned()); + table.AddColumn(new TableColumn("[bold yellow]Serial[/]").LeftAligned()); + table.AddColumn(new TableColumn("[bold yellow]Mfg Date[/]").LeftAligned()); table.AddColumn(new TableColumn("[bold yellow]Brightness[/]").Centered()); table.AddColumn(new TableColumn("[bold yellow]Contrast[/]").Centered()); } @@ -219,9 +230,29 @@ private static void OutputTableList(List monitors, bool verboseOutput) inputInfo }; - // Add brightness and contrast columns if verbose mode is enabled + // Add EDID and feature columns if verbose mode is enabled if (verboseOutput) { + // EDID information + string manufacturer = monitor.ManufacturerName != null + ? $"[cyan]{monitor.ManufacturerName}[/]" + : "[dim]N/A[/]"; + string model = monitor.ModelName != null + ? $"[cyan]{monitor.ModelName}[/]" + : "[dim]N/A[/]"; + string serial = monitor.SerialNumber != null + ? $"[dim]{monitor.SerialNumber}[/]" + : "[dim]N/A[/]"; + string mfgDate = monitor.ManufactureYear.HasValue + ? (monitor.ManufactureWeek.HasValue + ? $"[dim]{monitor.ManufactureYear}/W{monitor.ManufactureWeek}[/]" + : $"[dim]{monitor.ManufactureYear}[/]") + : "[dim]N/A[/]"; + + row.Add(manufacturer); + row.Add(model); + row.Add(serial); + row.Add(mfgDate); row.Add(brightnessInfo); row.Add(contrastInfo); } diff --git a/DDCSwitch/DDCSwitch.csproj b/DDCSwitch/DDCSwitch.csproj index c9d1921..835173e 100644 --- a/DDCSwitch/DDCSwitch.csproj +++ b/DDCSwitch/DDCSwitch.csproj @@ -12,6 +12,7 @@ false true ddcswitch + $(NoWarn);CA1416 diff --git a/DDCSwitch/EdidParser.cs b/DDCSwitch/EdidParser.cs new file mode 100644 index 0000000..e50d6eb --- /dev/null +++ b/DDCSwitch/EdidParser.cs @@ -0,0 +1,307 @@ +using System.Text; + +namespace DDCSwitch; + +/// +/// Parses EDID (Extended Display Identification Data) blocks to extract monitor information. +/// +public static class EdidParser +{ + /// + /// 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); + } + } + + 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; + } +} diff --git a/DDCSwitch/JsonContext.cs b/DDCSwitch/JsonContext.cs index 02a5a5c..da95367 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -47,7 +47,14 @@ internal record MonitorInfo( string? CurrentInputCode, string Status, string? Brightness = null, - string? Contrast = null); + string? Contrast = null, + string? ManufacturerId = null, + string? ManufacturerName = null, + string? ModelName = null, + string? SerialNumber = null, + int? ProductCode = null, + int? ManufactureYear = null, + int? ManufactureWeek = null); internal record MonitorReference(int Index, string Name, string DeviceName, bool IsPrimary = false); diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Monitor.cs index 00dab9a..19e0e22 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Monitor.cs @@ -16,9 +16,45 @@ public class Monitor(int index, string name, string deviceName, bool isPrimary, public string DeviceName { get; } = deviceName; public bool IsPrimary { get; } = isPrimary; + // EDID properties + public string? ManufacturerId { get; private set; } + public string? ManufacturerName { get; private set; } + public string? ModelName { get; private set; } + public string? SerialNumber { get; private set; } + public ushort? ProductCode { get; private set; } + public int? ManufactureYear { get; private set; } + public int? ManufactureWeek { get; private set; } + private IntPtr Handle { get; } = handle; private bool _disposed; + /// + /// Loads EDID data from registry and populates EDID properties. + /// + public void LoadEdidData() + { + try + { + var edid = NativeMethods.GetEdidFromRegistry(DeviceName); + if (edid == null || edid.Length < 128) return; + + ManufacturerId = EdidParser.ParseManufacturerId(edid); + if (ManufacturerId != null) + { + ManufacturerName = EdidParser.GetManufacturerName(ManufacturerId); + } + ModelName = EdidParser.ParseModelName(edid); + SerialNumber = EdidParser.ParseSerialNumber(edid); + ProductCode = EdidParser.ParseProductCode(edid); + ManufactureYear = EdidParser.ParseManufactureYear(edid); + ManufactureWeek = EdidParser.ParseManufactureWeek(edid); + } + catch + { + // Graceful degradation - EDID properties remain null + } + } + public bool TryGetInputSource(out uint currentValue, out uint maxValue) { currentValue = 0; diff --git a/DDCSwitch/MonitorController.cs b/DDCSwitch/MonitorController.cs index 0381ed4..b758021 100644 --- a/DDCSwitch/MonitorController.cs +++ b/DDCSwitch/MonitorController.cs @@ -54,6 +54,10 @@ public static List EnumerateMonitors() isPrimary, physicalMonitor.hPhysicalMonitor ); + + // Load EDID data for monitor identification + monitor.LoadEdidData(); + monitors.Add(monitor); } } diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/NativeMethods.cs index 5cdf881..a53aee0 100644 --- a/DDCSwitch/NativeMethods.cs +++ b/DDCSwitch/NativeMethods.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Microsoft.Win32; namespace DDCSwitch; @@ -85,5 +86,73 @@ public struct MONITORINFOEX } 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]; + } + + // Fallback: return first EDID if index doesn't match + return edidList.Count > 0 ? edidList[0] : null; + } + catch + { + return null; + } + } +} diff --git a/LICENSE b/LICENSE index 34771c0..2175917 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2026 ddcswitch Contributors +https://github.com/markdwags/ddcswitch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +20,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - From ff1b199a3c3aec35c6cab52e83b53fbe3b6fdb48 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:20:21 -0600 Subject: [PATCH 04/13] Add EDID information retrieval and display command for monitors --- DDCSwitch/Commands/CommandRouter.cs | 1 + DDCSwitch/Commands/ConsoleOutputFormatter.cs | 67 ++++++ DDCSwitch/Commands/HelpCommand.cs | 5 + DDCSwitch/Commands/InfoCommand.cs | 201 +++++++++++++++++ DDCSwitch/EdidParser.cs | 222 +++++++++++++++++++ DDCSwitch/JsonContext.cs | 46 ++++ DDCSwitch/Monitor.cs | 8 + EXAMPLES.md | 212 +++++++++++++++++- README.md | 25 ++- 9 files changed, 785 insertions(+), 2 deletions(-) create mode 100644 DDCSwitch/Commands/InfoCommand.cs diff --git a/DDCSwitch/Commands/CommandRouter.cs b/DDCSwitch/Commands/CommandRouter.cs index 0cefadb..a7d03d9 100644 --- a/DDCSwitch/Commands/CommandRouter.cs +++ b/DDCSwitch/Commands/CommandRouter.cs @@ -38,6 +38,7 @@ public static int Route(string[] args) "get" => GetCommand.Execute(filteredArgs, jsonOutput), "set" => SetCommand.Execute(filteredArgs, jsonOutput), "toggle" => ToggleCommand.Execute(filteredArgs, jsonOutput), + "info" => InfoCommand.Execute(filteredArgs, jsonOutput), "version" or "-v" or "--version" => HelpCommand.ShowVersion(jsonOutput), "help" or "-h" or "--help" or "/?" => HelpCommand.ShowUsage(), _ => InvalidCommand(filteredArgs[0], jsonOutput) diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 3a917f2..97dce0a 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -38,5 +38,72 @@ public static void WriteMonitorInfo(string label, string value, bool highlight = var color = highlight ? "yellow" : "cyan"; AnsiConsole.MarkupLine($" [bold {color}]{label}:[/] {value}"); } + + public static void WriteMonitorDetails(Monitor monitor) + { + WriteHeader($"Monitor {monitor.Index}: {monitor.Name}"); + + WriteMonitorInfo("Device Name", monitor.DeviceName); + WriteMonitorInfo("Is Primary", monitor.IsPrimary ? "Yes" : "No"); + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(" [bold underline yellow]EDID Information[/]"); + + if (monitor.EdidVersion != null) + WriteMonitorInfo("EDID Version", monitor.EdidVersion.ToString()); + + if (monitor.ManufacturerId != null) + WriteMonitorInfo("Manufacturer ID", monitor.ManufacturerId); + + if (monitor.ManufacturerName != null) + WriteMonitorInfo("Manufacturer", monitor.ManufacturerName); + + if (monitor.ModelName != null) + WriteMonitorInfo("Model Name", monitor.ModelName); + + if (monitor.SerialNumber != null) + WriteMonitorInfo("Serial Number", monitor.SerialNumber); + + if (monitor.ProductCode.HasValue) + WriteMonitorInfo("Product Code", $"0x{monitor.ProductCode.Value:X4}"); + + if (monitor.ManufactureYear.HasValue) + { + var date = monitor.ManufactureWeek.HasValue + ? $"{monitor.ManufactureYear.Value} Week {monitor.ManufactureWeek.Value}" + : $"{monitor.ManufactureYear.Value}"; + WriteMonitorInfo("Manufacture Date", date); + } + + if (monitor.VideoInputDefinition != null) + { + WriteMonitorInfo("Video Input Type", monitor.VideoInputDefinition.ToString()); + } + + if (monitor.SupportedFeatures != null) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(" [bold underline yellow]Supported Features[/]"); + WriteMonitorInfo("Display Type", monitor.SupportedFeatures.DisplayTypeDescription); + WriteMonitorInfo("DPMS Standby", monitor.SupportedFeatures.DpmsStandby ? "Supported" : "Not supported"); + WriteMonitorInfo("DPMS Suspend", monitor.SupportedFeatures.DpmsSuspend ? "Supported" : "Not supported"); + WriteMonitorInfo("DPMS Active-Off", monitor.SupportedFeatures.DpmsActiveOff ? "Supported" : "Not supported"); + WriteMonitorInfo("Default Color Space", monitor.SupportedFeatures.DefaultColorSpace ? "Standard" : "Non-standard"); + WriteMonitorInfo("Preferred Timing Mode", monitor.SupportedFeatures.PreferredTimingMode ? "Included" : "Not included"); + WriteMonitorInfo("Continuous Frequency", monitor.SupportedFeatures.ContinuousFrequency ? "Supported" : "Not supported"); + } + + if (monitor.Chromaticity != null) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(" [bold underline yellow]Chromaticity Coordinates (CIE 1931)[/]"); + WriteMonitorInfo("Red", monitor.Chromaticity.Red.ToString()); + WriteMonitorInfo("Green", monitor.Chromaticity.Green.ToString()); + WriteMonitorInfo("Blue", monitor.Chromaticity.Blue.ToString()); + WriteMonitorInfo("White Point", monitor.Chromaticity.White.ToString()); + } + + AnsiConsole.WriteLine(); + } } diff --git a/DDCSwitch/Commands/HelpCommand.cs b/DDCSwitch/Commands/HelpCommand.cs index 5b2697c..f51c21d 100644 --- a/DDCSwitch/Commands/HelpCommand.cs +++ b/DDCSwitch/Commands/HelpCommand.cs @@ -61,6 +61,9 @@ public static int ShowUsage() commandsTable.AddRow( "[cyan]toggle[/] [green][/] [blue][/] [blue][/]", "Toggle between two input sources automatically"); + commandsTable.AddRow( + "[cyan]info[/] [green][/]", + "Show detailed EDID information for a specific monitor"); commandsTable.AddRow( "[cyan]version[/] [dim]or[/] [cyan]-v[/]", "Display version information"); @@ -95,6 +98,8 @@ public static int ShowUsage() AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch set 1 brightness 75%"); AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch toggle 0 HDMI1 DP1"); AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch toggle \"Dell Monitor\" HDMI1 HDMI2"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch info 0"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch info 0 --json"); return 0; } diff --git a/DDCSwitch/Commands/InfoCommand.cs b/DDCSwitch/Commands/InfoCommand.cs new file mode 100644 index 0000000..9720fba --- /dev/null +++ b/DDCSwitch/Commands/InfoCommand.cs @@ -0,0 +1,201 @@ +using Spectre.Console; +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class InfoCommand +{ + public static int Execute(string[] args, bool jsonOutput) + { + if (args.Length < 2) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, "Monitor identifier required"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError("Monitor identifier required."); + AnsiConsole.WriteLine("Usage: ddcswitch info "); + } + + return 1; + } + + List monitors; + + if (!jsonOutput) + { + monitors = null!; + AnsiConsole.Status() + .Start("Enumerating monitors...", ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("cyan")); + monitors = MonitorController.EnumerateMonitors(); + }); + } + else + { + monitors = MonitorController.EnumerateMonitors(); + } + + if (monitors.Count == 0) + { + return HandleNoMonitors(jsonOutput); + } + + var monitor = MonitorController.FindMonitor(monitors, args[1]); + + if (monitor == null) + { + return HandleMonitorNotFound(monitors, args[1], jsonOutput); + } + + int result = DisplayMonitorInfo(monitor, jsonOutput); + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return result; + } + + private static int DisplayMonitorInfo(Monitor monitor, bool jsonOutput) + { + try + { + // Get current input source + string? currentInput = null; + uint? currentInputCode = null; + string status = "ok"; + + if (monitor.TryGetInputSource(out uint current, out _)) + { + currentInput = InputSource.GetName(current); + currentInputCode = current; + } + else + { + status = "no_ddc_ci"; + } + + if (jsonOutput) + { + var monitorRef = new MonitorReference( + monitor.Index, + monitor.Name, + monitor.DeviceName, + monitor.IsPrimary); + + var edidInfo = new EdidInfo( + monitor.EdidVersion?.Major, + monitor.EdidVersion?.Minor, + monitor.ManufacturerId, + monitor.ManufacturerName, + monitor.ModelName, + monitor.SerialNumber, + monitor.ProductCode.HasValue ? $"0x{monitor.ProductCode.Value:X4}" : null, + monitor.ManufactureYear, + monitor.ManufactureWeek, + monitor.VideoInputDefinition?.IsDigital, + monitor.VideoInputDefinition?.ToString(), + monitor.SupportedFeatures != null ? new FeaturesInfo( + monitor.SupportedFeatures.DisplayTypeDescription, + monitor.SupportedFeatures.DpmsStandby, + monitor.SupportedFeatures.DpmsSuspend, + monitor.SupportedFeatures.DpmsActiveOff, + monitor.SupportedFeatures.DefaultColorSpace, + monitor.SupportedFeatures.PreferredTimingMode, + monitor.SupportedFeatures.ContinuousFrequency) : null, + monitor.Chromaticity != null ? new ChromaticityInfo( + new ColorPointInfo(monitor.Chromaticity.Red.X, monitor.Chromaticity.Red.Y), + new ColorPointInfo(monitor.Chromaticity.Green.X, monitor.Chromaticity.Green.Y), + new ColorPointInfo(monitor.Chromaticity.Blue.X, monitor.Chromaticity.Blue.Y), + new ColorPointInfo(monitor.Chromaticity.White.X, monitor.Chromaticity.White.Y)) : null); + + var response = new MonitorInfoResponse( + true, + monitorRef, + status, + currentInput, + currentInputCode.HasValue ? $"0x{currentInputCode.Value:X2}" : null, + edidInfo); + + Console.WriteLine(JsonSerializer.Serialize(response, JsonContext.Default.MonitorInfoResponse)); + } + else + { + ConsoleOutputFormatter.WriteMonitorDetails(monitor); + + if (status == "ok") + { + AnsiConsole.MarkupLine(" [bold underline yellow]Current Status[/]"); + ConsoleOutputFormatter.WriteMonitorInfo("Current Input", $"{currentInput} (0x{currentInputCode:X2})"); + } + else + { + AnsiConsole.MarkupLine(" [bold yellow]Warning:[/] [yellow]DDC/CI communication not available[/]"); + } + + AnsiConsole.WriteLine(); + } + + return 0; + } + catch (Exception ex) + { + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, 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)); + } + else + { + ConsoleOutputFormatter.WriteError($"Failed to retrieve monitor information: {ex.Message}"); + } + + return 1; + } + } + + private static int HandleNoMonitors(bool jsonOutput) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, "No DDC/CI capable monitors found"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError("No DDC/CI capable monitors found."); + } + + return 1; + } + + private static int HandleMonitorNotFound(List monitors, string identifier, bool jsonOutput) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, $"Monitor '{identifier}' not found"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError($"Monitor '{identifier}' not found."); + AnsiConsole.MarkupLine($"Available monitors: [cyan]{string.Join(", ", monitors.Select(m => m.Index.ToString()))}[/]"); + } + + foreach (var m in monitors) + { + m.Dispose(); + } + + return 1; + } +} diff --git a/DDCSwitch/EdidParser.cs b/DDCSwitch/EdidParser.cs index e50d6eb..344a598 100644 --- a/DDCSwitch/EdidParser.cs +++ b/DDCSwitch/EdidParser.cs @@ -2,6 +2,68 @@ 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); + /// /// Parses EDID (Extended Display Identification Data) blocks to extract monitor information. /// @@ -194,9 +256,37 @@ public static string GetManufacturerName(string? manufacturerId) } } + // 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. /// @@ -304,4 +394,136 @@ public static bool ValidateHeader(byte[] edid) 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 X + byte lsb2 = edid[26]; // Low-order bits for red/green Y + + // Red X: bits 7-6 of byte 27 (MSB) + all 8 bits of byte 25 bits 1-0 (LSB) + // Red Y: bits 5-4 of byte 27 (MSB) + all 8 bits of byte 26 bits 1-0 (LSB) + int redXRaw = ((edid[27] & 0xC0) << 2) | ((lsb >> 6) & 0x03); + int redYRaw = ((edid[27] & 0x30) << 4) | ((lsb2 >> 6) & 0x03); + + // Green X: bits 7-6 of byte 28 + byte 25 bits 5-4 + // Green Y: bits 5-4 of byte 28 + byte 26 bits 5-4 + int greenXRaw = ((edid[28] & 0xC0) << 2) | ((lsb >> 4) & 0x03); + int greenYRaw = ((edid[28] & 0x30) << 4) | ((lsb2 >> 4) & 0x03); + + // Blue X: bits 7-6 of byte 29 + byte 25 bits 3-2 + // Blue Y: bits 5-4 of byte 29 + byte 26 bits 3-2 + int blueXRaw = ((edid[29] & 0xC0) << 2) | ((lsb >> 2) & 0x03); + int blueYRaw = ((edid[29] & 0x30) << 4) | ((lsb2 >> 2) & 0x03); + + // White X: bits 7-6 of byte 30 + byte 25 bits 1-0 + // White Y: bits 5-4 of byte 30 + byte 26 bits 1-0 + int whiteXRaw = ((edid[30] & 0xC0) << 2) | (lsb & 0x03); + int whiteYRaw = ((edid[30] & 0x30) << 4) | (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/JsonContext.cs b/DDCSwitch/JsonContext.cs index da95367..9ea4b3b 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -12,6 +12,11 @@ namespace DDCSwitch; [JsonSerializable(typeof(VcpFeatureInfo))] [JsonSerializable(typeof(VcpFeatureType))] [JsonSerializable(typeof(MonitorReference))] +[JsonSerializable(typeof(MonitorInfoResponse))] +[JsonSerializable(typeof(EdidInfo))] +[JsonSerializable(typeof(FeaturesInfo))] +[JsonSerializable(typeof(ChromaticityInfo))] +[JsonSerializable(typeof(ColorPointInfo))] [JsonSourceGenerationOptions( WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, @@ -58,3 +63,44 @@ internal record MonitorInfo( internal record MonitorReference(int Index, string Name, string DeviceName, bool IsPrimary = false); +internal record MonitorInfoResponse( + bool Success, + MonitorReference Monitor, + string Status, + string? CurrentInput = null, + string? CurrentInputCode = null, + EdidInfo? Edid = null, + string? ErrorMessage = null); + +internal record EdidInfo( + byte? VersionMajor, + byte? VersionMinor, + string? ManufacturerId, + string? ManufacturerName, + string? ModelName, + string? SerialNumber, + string? ProductCode, + int? ManufactureYear, + int? ManufactureWeek, + bool? IsDigitalInput, + string? VideoInputType, + FeaturesInfo? Features, + ChromaticityInfo? Chromaticity); + +internal record FeaturesInfo( + string DisplayType, + bool DpmsStandby, + bool DpmsSuspend, + bool DpmsActiveOff, + bool DefaultColorSpace, + bool PreferredTimingMode, + bool ContinuousFrequency); + +internal record ChromaticityInfo( + ColorPointInfo Red, + ColorPointInfo Green, + ColorPointInfo Blue, + ColorPointInfo White); + +internal record ColorPointInfo(double X, double Y); + diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Monitor.cs index 19e0e22..5e9a6e6 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Monitor.cs @@ -24,6 +24,10 @@ public class Monitor(int index, string name, string deviceName, bool isPrimary, public ushort? ProductCode { get; private set; } public int? ManufactureYear { get; private set; } public int? ManufactureWeek { get; private set; } + public EdidVersion? EdidVersion { get; private set; } + public VideoInputDefinition? VideoInputDefinition { get; private set; } + public SupportedFeatures? SupportedFeatures { get; private set; } + public ChromaticityCoordinates? Chromaticity { get; private set; } private IntPtr Handle { get; } = handle; private bool _disposed; @@ -48,6 +52,10 @@ public void LoadEdidData() 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); } catch { diff --git a/EXAMPLES.md b/EXAMPLES.md index 5c4c8b8..b90b2d6 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,6 +1,216 @@ # ddcswitch Examples -This document contains detailed examples and use cases for ddcswitch, including input switching, brightness/contrast control, comprehensive VCP feature access, 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, and automation. + +## Monitor Information (EDID) + +Retrieve detailed Extended Display Identification Data (EDID) from your monitors to view specifications, capabilities, and color characteristics. + +### Basic EDID Information + +View all EDID data for a specific monitor: + +```powershell +ddcswitch info 0 +``` + +Example output: +``` +── Monitor 0: Generic PnP Monitor ─────────────────────────────── + Device Name: \\.\DISPLAY2 + Is Primary: No + + EDID Information + EDID Version: 1.4 + Manufacturer ID: ACR + Manufacturer: Acer Technologies + Model Name: VG270U P + Product Code: 0x06CF + Manufacture Date: 2021 Week 2 + Video Input Type: Digital + + Supported Features + Display Type: RGB Color + DPMS Standby: Supported + DPMS Suspend: Not supported + DPMS Active-Off: Supported + Default Color Space: Standard + Preferred Timing Mode: Included + Continuous Frequency: Supported + + Chromaticity Coordinates (CIE 1931) + Red: x=0.6396, y=0.3300 + Green: x=0.2998, y=0.5996 + Blue: x=0.1503, y=0.0595 + White Point: x=0.3125, y=0.3291 + + Current Status + Current Input: HDMI1 (0x11) +``` + +### JSON Output for EDID Data + +Retrieve EDID data in JSON format for programmatic access: + +```powershell +ddcswitch info 0 --json +``` + +Example JSON output: +```json +{ + "success": true, + "monitor": { + "index": 0, + "name": "Generic PnP Monitor", + "deviceName": "\\\\.\\DISPLAY2", + "isPrimary": false + }, + "status": "ok", + "currentInput": "HDMI1", + "currentInputCode": "0x11", + "edid": { + "versionMajor": 1, + "versionMinor": 4, + "manufacturerId": "ACR", + "manufacturerName": "Acer Technologies", + "modelName": "VG270U P", + "productCode": "0x06CF", + "manufactureYear": 2021, + "manufactureWeek": 2, + "isDigitalInput": true, + "videoInputType": "Digital", + "features": { + "displayType": "RGB Color", + "dpmsStandby": false, + "dpmsSuspend": false, + "dpmsActiveOff": true, + "defaultColorSpace": false, + "preferredTimingMode": true, + "continuousFrequency": true + }, + "chromaticity": { + "red": { "x": 0.6396484375, "y": 0.330078125 }, + "green": { "x": 0.2998046875, "y": 0.599609375 }, + "blue": { "x": 0.150390625, "y": 0.0595703125 }, + "white": { "x": 0.3125, "y": 0.32910156 } + } + } +} +``` + +### Automation Examples with EDID Data + +#### PowerShell: Check Monitor Model Before Applying Settings + +```powershell +# Get EDID info and apply settings only to specific model +$info = ddcswitch info 0 --json | ConvertFrom-Json + +if ($info.edid.modelName -like "*VG270U*") { + Write-Host "Configuring Acer VG270U..." -ForegroundColor Green + ddcswitch set 0 brightness 80% + ddcswitch set 0 contrast 75% + ddcswitch set 0 input HDMI1 +} +else { + Write-Host "Monitor model: $($info.edid.modelName)" -ForegroundColor Yellow +} +``` + +#### PowerShell: Log Monitor Information + +```powershell +# Create monitor inventory with EDID details +$monitors = @() +$listOutput = ddcswitch list --json | ConvertFrom-Json + +foreach ($monitor in $listOutput.monitors) { + $edidInfo = ddcswitch info $monitor.index --json | ConvertFrom-Json + + $monitors += [PSCustomObject]@{ + Index = $monitor.index + Name = $monitor.name + Manufacturer = $edidInfo.edid.manufacturerName + Model = $edidInfo.edid.modelName + Serial = $edidInfo.edid.serialNumber + EdidVersion = "$($edidInfo.edid.versionMajor).$($edidInfo.edid.versionMinor)" + VideoInput = $edidInfo.edid.videoInputType + ManufactureDate = "$($edidInfo.edid.manufactureYear) Week $($edidInfo.edid.manufactureWeek)" + CurrentInput = $edidInfo.currentInput + } +} + +$monitors | Format-Table -AutoSize +``` + +#### Python: Color Calibration Using Chromaticity Data + +```python +import subprocess +import json + +def get_monitor_chromaticity(monitor_index): + """Get chromaticity coordinates for color calibration.""" + result = subprocess.run( + ['ddcswitch', 'info', str(monitor_index), '--json'], + capture_output=True, + text=True + ) + + data = json.loads(result.stdout) + if data['success'] and 'chromaticity' in data['edid']: + chroma = data['edid']['chromaticity'] + return { + 'red': (chroma['red']['x'], chroma['red']['y']), + 'green': (chroma['green']['x'], chroma['green']['y']), + 'blue': (chroma['blue']['x'], chroma['blue']['y']), + 'white': (chroma['white']['x'], chroma['white']['y']) + } + return None + +# Use chromaticity data for color management +chroma = get_monitor_chromaticity(0) +if chroma: + print(f"Monitor Color Gamut:") + print(f" Red: x={chroma['red'][0]:.4f}, y={chroma['red'][1]:.4f}") + print(f" Green: x={chroma['green'][0]:.4f}, y={chroma['green'][1]:.4f}") + print(f" Blue: x={chroma['blue'][0]:.4f}, y={chroma['blue'][1]:.4f}") + print(f" White: x={chroma['white'][0]:.4f}, y={chroma['white'][1]:.4f}") +``` + +#### Node.js: Monitor Fleet Management + +```javascript +const { execSync } = require('child_process'); + +function getMonitorInfo(index) { + const output = execSync(`ddcswitch info ${index} --json`, { encoding: 'utf8' }); + return JSON.parse(output); +} + +// Create monitor inventory +function inventoryMonitors() { + const list = JSON.parse(execSync('ddcswitch list --json', { encoding: 'utf8' })); + + const inventory = list.monitors.map(monitor => { + const info = getMonitorInfo(monitor.index); + return { + index: monitor.index, + manufacturer: info.edid?.manufacturerName || 'Unknown', + model: info.edid?.modelName || 'Unknown', + edidVersion: `${info.edid?.versionMajor}.${info.edid?.versionMinor}`, + isDigital: info.edid?.isDigitalInput, + manufactureYear: info.edid?.manufactureYear, + currentInput: info.currentInput + }; + }); + + console.table(inventory); +} + +inventoryMonitors(); +``` ## Comprehensive VCP Feature Examples diff --git a/README.md b/README.md index 53ed458..547221c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ A Windows command-line utility to control monitor settings via DDC/CI (Display D ## Features - 🖥️ **List all DDC/CI capable monitors** with their current input sources -- 🔄 **Switch monitor inputs** programmatically (HDMI, DisplayPort, DVI, VGA, etc.) +- � **Detailed 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.) @@ -103,6 +104,28 @@ Example output: 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: + +```powershell +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: From 06e264fb7dbe4a82e20eb783bef7e4366219585b Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:22:03 -0600 Subject: [PATCH 05/13] Add info command to display detailed EDID information with JSON output support --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb9193..a2904b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to ddcswitch will be documented in this file. ### Added - Added the toggle command to flip a monitor between two input sources +- `info` command to display detailed EDID (Extended Display Identification Data) information + - Full JSON output support for programmatic access ## [1.0.2] - 2026-01-07 From f7a2556030e2ce0ed3a7b7734e2fd82e524d9578 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:26:38 -0600 Subject: [PATCH 06/13] Refactor manufacture date display to include month name and week number --- DDCSwitch/Commands/ConsoleOutputFormatter.cs | 31 +++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 97dce0a..5b44563 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -70,7 +70,7 @@ public static void WriteMonitorDetails(Monitor monitor) if (monitor.ManufactureYear.HasValue) { var date = monitor.ManufactureWeek.HasValue - ? $"{monitor.ManufactureYear.Value} Week {monitor.ManufactureWeek.Value}" + ? FormatManufactureDate(monitor.ManufactureYear.Value, monitor.ManufactureWeek.Value) : $"{monitor.ManufactureYear.Value}"; WriteMonitorInfo("Manufacture Date", date); } @@ -105,5 +105,34 @@ public static void WriteMonitorDetails(Monitor monitor) AnsiConsole.WriteLine(); } + + private static string FormatManufactureDate(int year, int week) + { + // Calculate approximate month from week number + // Week 1-4: January, Week 5-8: February, etc. + string? monthName = week switch + { + >= 1 and <= 4 => "January", + >= 5 and <= 8 => "February", + >= 9 and <= 13 => "March", + >= 14 and <= 17 => "April", + >= 18 and <= 22 => "May", + >= 23 and <= 26 => "June", + >= 27 and <= 30 => "July", + >= 31 and <= 35 => "August", + >= 36 and <= 39 => "September", + >= 40 and <= 43 => "October", + >= 44 and <= 48 => "November", + >= 49 and <= 53 => "December", + _ => null + }; + + if (monthName != null) + { + return $"{monthName} {year} (Week {week})"; + } + + return $"{year} Week {week}"; + } } From 4454b6f9416d7aa612f0d0a15caf9368f353dda8 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:29:23 -0600 Subject: [PATCH 07/13] Enhance monitor details display with structured panels and tables for better readability --- DDCSwitch/Commands/ConsoleOutputFormatter.cs | 154 +++++++++++++++---- DDCSwitch/Commands/InfoCommand.cs | 22 ++- 2 files changed, 139 insertions(+), 37 deletions(-) diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 5b44563..97d6e2d 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -41,69 +41,155 @@ public static void WriteMonitorInfo(string label, string value, bool highlight = public static void WriteMonitorDetails(Monitor monitor) { - WriteHeader($"Monitor {monitor.Index}: {monitor.Name}"); - - WriteMonitorInfo("Device Name", monitor.DeviceName); - WriteMonitorInfo("Is Primary", monitor.IsPrimary ? "Yes" : "No"); - + // Header with monitor identification + var headerPanel = new Panel( + $"[bold white]{monitor.Name}[/]\n" + + $"[dim]Device:[/] [cyan]{monitor.DeviceName}[/] " + + $"[dim]Primary:[/] {(monitor.IsPrimary ? "[green]Yes[/]" : "[dim]No[/]")}") + { + Header = new PanelHeader($"[bold cyan]Monitor {monitor.Index}[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Cyan1) + }; + AnsiConsole.Write(headerPanel); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine(" [bold underline yellow]EDID Information[/]"); - + + // EDID Information in a table + var edidTable = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.Grey) + .AddColumn(new TableColumn("[bold yellow]Property[/]").Width(20)) + .AddColumn(new TableColumn("[bold yellow]Value[/]")); + if (monitor.EdidVersion != null) - WriteMonitorInfo("EDID Version", monitor.EdidVersion.ToString()); - - if (monitor.ManufacturerId != null) - WriteMonitorInfo("Manufacturer ID", monitor.ManufacturerId); + edidTable.AddRow("[cyan]EDID Version[/]", $"[white]{monitor.EdidVersion}[/]"); if (monitor.ManufacturerName != null) - WriteMonitorInfo("Manufacturer", monitor.ManufacturerName); + edidTable.AddRow("[cyan]Manufacturer[/]", $"[white]{monitor.ManufacturerName}[/] [dim]({monitor.ManufacturerId})[/]"); + else if (monitor.ManufacturerId != null) + edidTable.AddRow("[cyan]Manufacturer ID[/]", $"[white]{monitor.ManufacturerId}[/]"); if (monitor.ModelName != null) - WriteMonitorInfo("Model Name", monitor.ModelName); + edidTable.AddRow("[cyan]Model Name[/]", $"[white]{monitor.ModelName}[/]"); if (monitor.SerialNumber != null) - WriteMonitorInfo("Serial Number", monitor.SerialNumber); + edidTable.AddRow("[cyan]Serial Number[/]", $"[white]{monitor.SerialNumber}[/]"); if (monitor.ProductCode.HasValue) - WriteMonitorInfo("Product Code", $"0x{monitor.ProductCode.Value:X4}"); + edidTable.AddRow("[cyan]Product Code[/]", $"[white]0x{monitor.ProductCode.Value:X4}[/]"); if (monitor.ManufactureYear.HasValue) { var date = monitor.ManufactureWeek.HasValue ? FormatManufactureDate(monitor.ManufactureYear.Value, monitor.ManufactureWeek.Value) : $"{monitor.ManufactureYear.Value}"; - WriteMonitorInfo("Manufacture Date", date); + edidTable.AddRow("[cyan]Manufacture Date[/]", $"[white]{date}[/]"); } if (monitor.VideoInputDefinition != null) { - WriteMonitorInfo("Video Input Type", monitor.VideoInputDefinition.ToString()); + var inputColor = monitor.VideoInputDefinition.IsDigital ? "green" : "yellow"; + edidTable.AddRow("[cyan]Video Input[/]", $"[{inputColor}]{monitor.VideoInputDefinition}[/]"); } - + + var edidPanel = new Panel(edidTable) + { + Header = new PanelHeader("[bold yellow]📋 EDID Information[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(edidPanel); + AnsiConsole.WriteLine(); + + // Supported Features if (monitor.SupportedFeatures != null) { + var featuresGrid = new Grid(); + featuresGrid.AddColumn(new GridColumn().Width(25)); + featuresGrid.AddColumn(new GridColumn()); + + featuresGrid.AddRow( + "[cyan]Display Type:[/]", + $"[white]{monitor.SupportedFeatures.DisplayTypeDescription}[/]"); + + featuresGrid.AddRow( + "[cyan]DPMS Standby:[/]", + FormatFeatureSupport(monitor.SupportedFeatures.DpmsStandby)); + + featuresGrid.AddRow( + "[cyan]DPMS Suspend:[/]", + FormatFeatureSupport(monitor.SupportedFeatures.DpmsSuspend)); + + featuresGrid.AddRow( + "[cyan]DPMS Active-Off:[/]", + FormatFeatureSupport(monitor.SupportedFeatures.DpmsActiveOff)); + + featuresGrid.AddRow( + "[cyan]Default Color Space:[/]", + monitor.SupportedFeatures.DefaultColorSpace ? "[green]Standard[/]" : "[dim]Non-standard[/]"); + + featuresGrid.AddRow( + "[cyan]Preferred Timing:[/]", + monitor.SupportedFeatures.PreferredTimingMode ? "[green]✓ Included[/]" : "[dim]Not included[/]"); + + featuresGrid.AddRow( + "[cyan]Continuous Frequency:[/]", + FormatFeatureSupport(monitor.SupportedFeatures.ContinuousFrequency)); + + var featuresPanel = new Panel(featuresGrid) + { + Header = new PanelHeader("[bold magenta]⚡ Supported Features[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Magenta) + }; + AnsiConsole.Write(featuresPanel); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine(" [bold underline yellow]Supported Features[/]"); - WriteMonitorInfo("Display Type", monitor.SupportedFeatures.DisplayTypeDescription); - WriteMonitorInfo("DPMS Standby", monitor.SupportedFeatures.DpmsStandby ? "Supported" : "Not supported"); - WriteMonitorInfo("DPMS Suspend", monitor.SupportedFeatures.DpmsSuspend ? "Supported" : "Not supported"); - WriteMonitorInfo("DPMS Active-Off", monitor.SupportedFeatures.DpmsActiveOff ? "Supported" : "Not supported"); - WriteMonitorInfo("Default Color Space", monitor.SupportedFeatures.DefaultColorSpace ? "Standard" : "Non-standard"); - WriteMonitorInfo("Preferred Timing Mode", monitor.SupportedFeatures.PreferredTimingMode ? "Included" : "Not included"); - WriteMonitorInfo("Continuous Frequency", monitor.SupportedFeatures.ContinuousFrequency ? "Supported" : "Not supported"); } - + + // Chromaticity Coordinates if (monitor.Chromaticity != null) { + 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)); + + chromaTable.AddRow( + "[red]● Red[/]", + $"[white]{monitor.Chromaticity.Red.X:F4}[/]", + $"[white]{monitor.Chromaticity.Red.Y:F4}[/]"); + + chromaTable.AddRow( + "[green]● Green[/]", + $"[white]{monitor.Chromaticity.Green.X:F4}[/]", + $"[white]{monitor.Chromaticity.Green.Y:F4}[/]"); + + chromaTable.AddRow( + "[blue]● Blue[/]", + $"[white]{monitor.Chromaticity.Blue.X:F4}[/]", + $"[white]{monitor.Chromaticity.Blue.Y:F4}[/]"); + + chromaTable.AddRow( + "[grey]● White[/]", + $"[white]{monitor.Chromaticity.White.X:F4}[/]", + $"[white]{monitor.Chromaticity.White.Y:F4}[/]"); + + var chromaPanel = new Panel(chromaTable) + { + Header = new PanelHeader("[bold blue]🎨 Chromaticity Coordinates (CIE 1931)[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Blue) + }; + AnsiConsole.Write(chromaPanel); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine(" [bold underline yellow]Chromaticity Coordinates (CIE 1931)[/]"); - WriteMonitorInfo("Red", monitor.Chromaticity.Red.ToString()); - WriteMonitorInfo("Green", monitor.Chromaticity.Green.ToString()); - WriteMonitorInfo("Blue", monitor.Chromaticity.Blue.ToString()); - WriteMonitorInfo("White Point", monitor.Chromaticity.White.ToString()); } - - AnsiConsole.WriteLine(); + } + + private static string FormatFeatureSupport(bool supported) + { + return supported ? "[green]✓ Supported[/]" : "[dim]✗ Not supported[/]"; } private static string FormatManufactureDate(int year, int week) diff --git a/DDCSwitch/Commands/InfoCommand.cs b/DDCSwitch/Commands/InfoCommand.cs index 9720fba..36e431e 100644 --- a/DDCSwitch/Commands/InfoCommand.cs +++ b/DDCSwitch/Commands/InfoCommand.cs @@ -131,14 +131,30 @@ private static int DisplayMonitorInfo(Monitor monitor, bool jsonOutput) { ConsoleOutputFormatter.WriteMonitorDetails(monitor); + // Current Status Panel if (status == "ok") { - AnsiConsole.MarkupLine(" [bold underline yellow]Current Status[/]"); - ConsoleOutputFormatter.WriteMonitorInfo("Current Input", $"{currentInput} (0x{currentInputCode:X2})"); + var statusPanel = new Panel( + $"[green]✓ DDC/CI Active[/]\n" + + $"[cyan]Current Input:[/] [white]{currentInput}[/] [dim](0x{currentInputCode:X2})[/]") + { + Header = new PanelHeader("[bold green]📡 Current Status[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Green) + }; + AnsiConsole.Write(statusPanel); } else { - AnsiConsole.MarkupLine(" [bold yellow]Warning:[/] [yellow]DDC/CI communication not available[/]"); + var warningPanel = new Panel( + "[yellow]DDC/CI communication not available[/]\n" + + "[dim]Input source switching may not be supported on this monitor[/]") + { + Header = new PanelHeader("[bold yellow]⚠ Warning[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(warningPanel); } AnsiConsole.WriteLine(); From 78866126e97a852279b340ba56a15459b8c46f6f Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:45:38 -0600 Subject: [PATCH 08/13] Improve error handling and performance in command processing and feature mapping --- DDCSwitch/Commands/CommandRouter.cs | 27 ++++++++++++++++++++++++++- DDCSwitch/Commands/VcpScanCommand.cs | 13 +++++++++---- DDCSwitch/FeatureResolver.cs | 23 +++++++++++++++-------- DDCSwitch/NativeMethods.cs | 18 +++++++++++++++++- DDCSwitch/VcpFeature.Generated.cs | 4 +++- 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/DDCSwitch/Commands/CommandRouter.cs b/DDCSwitch/Commands/CommandRouter.cs index a7d03d9..9c46922 100644 --- a/DDCSwitch/Commands/CommandRouter.cs +++ b/DDCSwitch/Commands/CommandRouter.cs @@ -44,6 +44,32 @@ public static int Route(string[] args) _ => InvalidCommand(filteredArgs[0], jsonOutput) }; } + catch (ArgumentException ex) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, ex.Message); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(ex.Message); + } + return 1; + } + catch (InvalidOperationException ex) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, ex.Message); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(ex.Message); + } + return 1; + } catch (Exception ex) { if (jsonOutput) @@ -55,7 +81,6 @@ public static int Route(string[] args) { ConsoleOutputFormatter.WriteError(ex.Message); } - return 1; } } diff --git a/DDCSwitch/Commands/VcpScanCommand.cs b/DDCSwitch/Commands/VcpScanCommand.cs index 971465e..b2466ae 100644 --- a/DDCSwitch/Commands/VcpScanCommand.cs +++ b/DDCSwitch/Commands/VcpScanCommand.cs @@ -108,10 +108,15 @@ public static int ScanSingleMonitor(string monitorIdentifier, bool jsonOutput) } // Filter only supported features for cleaner output - var supportedFeatures = features.Values - .Where(f => f.IsSupported) - .OrderBy(f => f.Code) - .ToList(); + var supportedFeatures = new List(features.Count); + foreach (var feature in features.Values) + { + if (feature.IsSupported) + { + supportedFeatures.Add(feature); + } + } + supportedFeatures.Sort((a, b) => a.Code.CompareTo(b.Code)); if (jsonOutput) { diff --git a/DDCSwitch/FeatureResolver.cs b/DDCSwitch/FeatureResolver.cs index ffb3f51..26c6af4 100644 --- a/DDCSwitch/FeatureResolver.cs +++ b/DDCSwitch/FeatureResolver.cs @@ -15,7 +15,7 @@ public static class FeatureResolver /// private static Dictionary BuildFeatureMap() { - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + var map = new Dictionary(capacity: 200, StringComparer.OrdinalIgnoreCase); foreach (var feature in VcpFeature.AllFeatures) { @@ -37,7 +37,7 @@ private static Dictionary BuildFeatureMap() /// private static Dictionary BuildCodeMap() { - var map = new Dictionary(); + var map = new Dictionary(capacity: 150); foreach (var feature in VcpFeature.AllFeatures) { @@ -202,10 +202,11 @@ public static bool TryParseVcpCode(string input, out byte vcpCode) input = input.Trim(); // Try hex format (0x10, 0X10) - if (input.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || - input.StartsWith("0X", StringComparison.OrdinalIgnoreCase)) + ReadOnlySpan inputSpan = input.AsSpan(); + if (inputSpan.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || + inputSpan.StartsWith("0X", StringComparison.OrdinalIgnoreCase)) { - var hexPart = input.Substring(2); + ReadOnlySpan hexPart = inputSpan.Slice(2); if (byte.TryParse(hexPart, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out vcpCode)) { // VCP codes are inherently valid for byte range (0x00-0xFF) @@ -246,12 +247,18 @@ public static bool TryParsePercentage(string input, out uint percentage) input = input.Trim(); // Remove % suffix if present - if (input.EndsWith("%")) + ReadOnlySpan inputSpan = input.AsSpan(); + if (inputSpan.EndsWith("%")) { - input = input.Substring(0, input.Length - 1).Trim(); + inputSpan = inputSpan.Slice(0, inputSpan.Length - 1); + // Trim trailing whitespace after removing % + while (inputSpan.Length > 0 && char.IsWhiteSpace(inputSpan[inputSpan.Length - 1])) + { + inputSpan = inputSpan.Slice(0, inputSpan.Length - 1); + } } - if (!uint.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out percentage)) + if (!uint.TryParse(inputSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out percentage)) { return false; } diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/NativeMethods.cs index a53aee0..bd3810b 100644 --- a/DDCSwitch/NativeMethods.cs +++ b/DDCSwitch/NativeMethods.cs @@ -150,8 +150,24 @@ public struct MONITORINFOEX // Fallback: return first EDID if index doesn't match return edidList.Count > 0 ? edidList[0] : null; } - catch + 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/VcpFeature.Generated.cs b/DDCSwitch/VcpFeature.Generated.cs index 554118c..c4a2723 100644 --- a/DDCSwitch/VcpFeature.Generated.cs +++ b/DDCSwitch/VcpFeature.Generated.cs @@ -779,7 +779,7 @@ public partial class VcpFeature /// /// All features registry - contains all predefined MCCS features /// - public static IReadOnlyList AllFeatures { get; } = new List + private static readonly VcpFeature[] _allFeatures = new VcpFeature[] { Degauss, NewControlValue, @@ -932,4 +932,6 @@ public partial class VcpFeature ScratchPad, VcpVersion }; + + public static IReadOnlyList AllFeatures => _allFeatures; } From fe10fa56b1d5b7707dcff29d726474b3292a261b Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:50:18 -0600 Subject: [PATCH 09/13] Remove outdated examples and improve documentation clarity for EDID data retrieval --- DDCSwitch/Monitor.cs | 3 -- EXAMPLES.md | 111 +------------------------------------------ README.md | 12 ----- 3 files changed, 1 insertion(+), 125 deletions(-) diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Monitor.cs index 5e9a6e6..2061fa6 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Monitor.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; namespace DDCSwitch; diff --git a/EXAMPLES.md b/EXAMPLES.md index b90b2d6..dc77bd8 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -14,40 +14,6 @@ View all EDID data for a specific monitor: ddcswitch info 0 ``` -Example output: -``` -── Monitor 0: Generic PnP Monitor ─────────────────────────────── - Device Name: \\.\DISPLAY2 - Is Primary: No - - EDID Information - EDID Version: 1.4 - Manufacturer ID: ACR - Manufacturer: Acer Technologies - Model Name: VG270U P - Product Code: 0x06CF - Manufacture Date: 2021 Week 2 - Video Input Type: Digital - - Supported Features - Display Type: RGB Color - DPMS Standby: Supported - DPMS Suspend: Not supported - DPMS Active-Off: Supported - Default Color Space: Standard - Preferred Timing Mode: Included - Continuous Frequency: Supported - - Chromaticity Coordinates (CIE 1931) - Red: x=0.6396, y=0.3300 - Green: x=0.2998, y=0.5996 - Blue: x=0.1503, y=0.0595 - White Point: x=0.3125, y=0.3291 - - Current Status - Current Input: HDMI1 (0x11) -``` - ### JSON Output for EDID Data Retrieve EDID data in JSON format for programmatic access: @@ -56,49 +22,6 @@ Retrieve EDID data in JSON format for programmatic access: ddcswitch info 0 --json ``` -Example JSON output: -```json -{ - "success": true, - "monitor": { - "index": 0, - "name": "Generic PnP Monitor", - "deviceName": "\\\\.\\DISPLAY2", - "isPrimary": false - }, - "status": "ok", - "currentInput": "HDMI1", - "currentInputCode": "0x11", - "edid": { - "versionMajor": 1, - "versionMinor": 4, - "manufacturerId": "ACR", - "manufacturerName": "Acer Technologies", - "modelName": "VG270U P", - "productCode": "0x06CF", - "manufactureYear": 2021, - "manufactureWeek": 2, - "isDigitalInput": true, - "videoInputType": "Digital", - "features": { - "displayType": "RGB Color", - "dpmsStandby": false, - "dpmsSuspend": false, - "dpmsActiveOff": true, - "defaultColorSpace": false, - "preferredTimingMode": true, - "continuousFrequency": true - }, - "chromaticity": { - "red": { "x": 0.6396484375, "y": 0.330078125 }, - "green": { "x": 0.2998046875, "y": 0.599609375 }, - "blue": { "x": 0.150390625, "y": 0.0595703125 }, - "white": { "x": 0.3125, "y": 0.32910156 } - } - } -} -``` - ### Automation Examples with EDID Data #### PowerShell: Check Monitor Model Before Applying Settings @@ -214,39 +137,7 @@ inventoryMonitors(); ## Comprehensive VCP Feature Examples -ddcswitch now supports all MCCS (Monitor Control Command Set) standardized VCP features, organized by categories for easy discovery. - -### VCP Feature Categories - -List all available feature categories: - -```powershell -ddcswitch list --categories -``` - -Output: -``` -Available VCP Feature Categories: -- ImageAdjustment: Brightness, contrast, sharpness, backlight controls -- ColorControl: RGB gains, color temperature, gamma, hue, saturation -- Geometry: Position, size, pincushion controls (mainly CRT monitors) -- Audio: Volume, mute, balance, treble, bass controls -- Preset: Factory defaults, degauss, calibration features -- Miscellaneous: Power mode, OSD settings, firmware information -``` - -### Browse Features by Category - -```powershell -# Image adjustment features -ddcswitch list --category image - -# Color control features -ddcswitch list --category color - -# Audio features -ddcswitch list --category audio -``` +ddcswitch now supports all MCCS (Monitor Control Command Set) standardized VCP features. ### Color Control Examples diff --git a/README.md b/README.md index 547221c..4e01591 100644 --- a/README.md +++ b/README.md @@ -194,18 +194,6 @@ The toggle command detects the current input and switches to the alternate one: Perfect for hotkeys and automation where you want to switch between two specific inputs without knowing which one is currently active. -Set brightness or contrast with percentage values: - -```powershell -# Set brightness to 75% -ddcswitch set 0 brightness 75% - -# Set contrast to 80% -ddcswitch set 0 contrast 80% -``` - -Output: `✓ Successfully set brightness to 75% (120/160)` - ### Raw VCP Access For advanced users, access any VCP feature by code: From 4efcd89b1d695282ccbc38797aeea2f9ea085eef Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:53:25 -0600 Subject: [PATCH 10/13] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 547221c..c60bc4c 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ A Windows command-line utility to control monitor settings via DDC/CI (Display D ## Features - 🖥️ **List all DDC/CI capable monitors** with their current input sources -- � **Detailed EDID information** - View monitor specifications, capabilities, and color characteristics -- �🔄 **Switch monitor inputs** programmatically (HDMI, DisplayPort, DVI, VGA, etc.) +- **Detailed 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.) From a474f12ece4afe0c7ce89fb5f9fbf6abf99b9e00 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:53:35 -0600 Subject: [PATCH 11/13] Update DDCSwitch/Commands/ToggleCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DDCSwitch/Commands/ToggleCommand.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/DDCSwitch/Commands/ToggleCommand.cs b/DDCSwitch/Commands/ToggleCommand.cs index e3dc3d3..e2c99aa 100644 --- a/DDCSwitch/Commands/ToggleCommand.cs +++ b/DDCSwitch/Commands/ToggleCommand.cs @@ -88,8 +88,6 @@ private static int HandleMissingArguments(int actualCount, bool jsonOutput) return 1; } - - private static int HandleInvalidInput(string invalidInput, string position, bool jsonOutput) { string errorMessage = $"Invalid {position} input source: '{invalidInput}'"; From 9fe5f4b302a81e7096be83190f2abcc2b19696c4 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:54:06 -0600 Subject: [PATCH 12/13] Update DDCSwitch/NativeMethods.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DDCSwitch/NativeMethods.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/NativeMethods.cs index bd3810b..da45c42 100644 --- a/DDCSwitch/NativeMethods.cs +++ b/DDCSwitch/NativeMethods.cs @@ -147,8 +147,8 @@ public struct MONITORINFOEX return edidList[listIndex]; } - // Fallback: return first EDID if index doesn't match - return edidList.Count > 0 ? edidList[0] : null; + // No reliable EDID mapping found for this display index + return null; } catch (System.Security.SecurityException) { From 0af6a6d56045ed1c5abf4146eaa04a165b368643 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:54:32 -0600 Subject: [PATCH 13/13] Update DDCSwitch/EdidParser.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DDCSwitch/EdidParser.cs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/DDCSwitch/EdidParser.cs b/DDCSwitch/EdidParser.cs index 344a598..00b182c 100644 --- a/DDCSwitch/EdidParser.cs +++ b/DDCSwitch/EdidParser.cs @@ -490,28 +490,24 @@ public static bool ValidateHeader(byte[] edid) { // 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 X - byte lsb2 = edid[26]; // Low-order bits for red/green Y + 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: bits 7-6 of byte 27 (MSB) + all 8 bits of byte 25 bits 1-0 (LSB) - // Red Y: bits 5-4 of byte 27 (MSB) + all 8 bits of byte 26 bits 1-0 (LSB) - int redXRaw = ((edid[27] & 0xC0) << 2) | ((lsb >> 6) & 0x03); - int redYRaw = ((edid[27] & 0x30) << 4) | ((lsb2 >> 6) & 0x03); + // 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: bits 7-6 of byte 28 + byte 25 bits 5-4 - // Green Y: bits 5-4 of byte 28 + byte 26 bits 5-4 - int greenXRaw = ((edid[28] & 0xC0) << 2) | ((lsb >> 4) & 0x03); - int greenYRaw = ((edid[28] & 0x30) << 4) | ((lsb2 >> 4) & 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: bits 7-6 of byte 29 + byte 25 bits 3-2 - // Blue Y: bits 5-4 of byte 29 + byte 26 bits 3-2 - int blueXRaw = ((edid[29] & 0xC0) << 2) | ((lsb >> 2) & 0x03); - int blueYRaw = ((edid[29] & 0x30) << 4) | ((lsb2 >> 2) & 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: bits 7-6 of byte 30 + byte 25 bits 1-0 - // White Y: bits 5-4 of byte 30 + byte 26 bits 1-0 - int whiteXRaw = ((edid[30] & 0xC0) << 2) | (lsb & 0x03); - int whiteYRaw = ((edid[30] & 0x30) << 4) | (lsb2 & 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);