diff --git a/CHANGELOG.md b/CHANGELOG.md index 392330f..a2904b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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 +- `info` command to display detailed EDID (Extended Display Identification Data) information + - Full JSON output support for programmatic access + ## [1.0.2] - 2026-01-07 ### Added diff --git a/DDCSwitch/Commands/CommandRouter.cs b/DDCSwitch/Commands/CommandRouter.cs index e9761c3..9c46922 100644 --- a/DDCSwitch/Commands/CommandRouter.cs +++ b/DDCSwitch/Commands/CommandRouter.cs @@ -37,11 +37,39 @@ 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), + "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) }; } + 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) @@ -53,7 +81,6 @@ public static int Route(string[] args) { ConsoleOutputFormatter.WriteError(ex.Message); } - return 1; } } diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 3a917f2..97d6e2d 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -38,5 +38,187 @@ 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) + { + // 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(); + + // 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) + edidTable.AddRow("[cyan]EDID Version[/]", $"[white]{monitor.EdidVersion}[/]"); + + if (monitor.ManufacturerName != null) + 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) + edidTable.AddRow("[cyan]Model Name[/]", $"[white]{monitor.ModelName}[/]"); + + if (monitor.SerialNumber != null) + edidTable.AddRow("[cyan]Serial Number[/]", $"[white]{monitor.SerialNumber}[/]"); + + if (monitor.ProductCode.HasValue) + 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}"; + edidTable.AddRow("[cyan]Manufacture Date[/]", $"[white]{date}[/]"); + } + + if (monitor.VideoInputDefinition != null) + { + 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(); + } + + // 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(); + } + } + + private static string FormatFeatureSupport(bool supported) + { + return supported ? "[green]βœ“ Supported[/]" : "[dim]βœ— Not supported[/]"; + } + + 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}"; + } } diff --git a/DDCSwitch/Commands/HelpCommand.cs b/DDCSwitch/Commands/HelpCommand.cs index 54895b0..f51c21d 100644 --- a/DDCSwitch/Commands/HelpCommand.cs +++ b/DDCSwitch/Commands/HelpCommand.cs @@ -58,6 +58,12 @@ 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]info[/] [green][/]", + "Show detailed EDID information for a specific monitor"); commandsTable.AddRow( "[cyan]version[/] [dim]or[/] [cyan]-v[/]", "Display version information"); @@ -90,6 +96,10 @@ 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"); + 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..36e431e --- /dev/null +++ b/DDCSwitch/Commands/InfoCommand.cs @@ -0,0 +1,217 @@ +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); + + // Current Status Panel + if (status == "ok") + { + 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 + { + 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(); + } + + 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/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/Commands/ToggleCommand.cs b/DDCSwitch/Commands/ToggleCommand.cs new file mode 100644 index 0000000..e2c99aa --- /dev/null +++ b/DDCSwitch/Commands/ToggleCommand.cs @@ -0,0 +1,399 @@ +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/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/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..00b182c --- /dev/null +++ b/DDCSwitch/EdidParser.cs @@ -0,0 +1,525 @@ +using System.Text; + +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. +/// +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); + } + } + + // If no descriptor serial found, try numeric serial at bytes 12-15 + var numericSerial = ParseNumericSerialNumber(edid); + if (numericSerial.HasValue && numericSerial.Value != 0) + { + return numericSerial.Value.ToString(); + } + + return null; + } + + /// + /// Parses the numeric serial number from EDID. + /// + /// EDID data (at least 16 bytes) + /// Numeric serial number or null if invalid + public static uint? ParseNumericSerialNumber(byte[] edid) + { + if (edid.Length < 16) return null; + + try + { + // Serial number is at bytes 12-15 (little-endian, 32-bit) + uint serial = (uint)(edid[12] | (edid[13] << 8) | (edid[14] << 16) | (edid[15] << 24)); + return serial; + } + catch + { + return null; + } + } + + /// + /// Parses the product code from EDID. + /// + /// EDID data (at least 12 bytes) + /// Product code as ushort or null if invalid + public static ushort? ParseProductCode(byte[] edid) + { + if (edid.Length < 12) return null; + + try + { + // Product code is at bytes 10-11 (little-endian) + return (ushort)(edid[10] | (edid[11] << 8)); + } + catch + { + return null; + } + } + + /// + /// Parses the manufacture year from EDID. + /// + /// EDID data (at least 18 bytes) + /// Manufacture year (e.g., 2023) or null if invalid + public static int? ParseManufactureYear(byte[] edid) + { + if (edid.Length < 18) return null; + + try + { + // Manufacture year is at byte 17, stored as offset from 1990 + int year = edid[17] + 1990; + return year >= 1990 && year <= 2100 ? year : null; + } + catch + { + return null; + } + } + + /// + /// Parses the manufacture week from EDID. + /// + /// EDID data (at least 17 bytes) + /// Manufacture week (1-53) or null if invalid + public static int? ParseManufactureWeek(byte[] edid) + { + if (edid.Length < 17) return null; + + try + { + // Manufacture week is at byte 16 (1-53, or 0xFF for unknown) + int week = edid[16]; + return week >= 1 && week <= 53 ? week : null; + } + catch + { + return null; + } + } + + /// + /// Helper to parse ASCII string from EDID descriptor block. + /// + private static string? ParseDescriptorString(byte[] edid, int offset) + { + try + { + var sb = new StringBuilder(); + for (int i = 0; i < 13; i++) + { + byte b = edid[offset + i]; + if (b == 0x0A || b == 0x00) break; // Newline or null terminator + if (b >= 0x20 && b <= 0x7E) // Printable ASCII + { + sb.Append((char)b); + } + } + + string result = sb.ToString().Trim(); + return string.IsNullOrWhiteSpace(result) ? null : result; + } + catch + { + return null; + } + } + + /// + /// Validates EDID header (first 8 bytes should be: 00 FF FF FF FF FF FF 00). + /// + /// EDID data (at least 8 bytes) + /// True if header is valid + public static bool ValidateHeader(byte[] edid) + { + if (edid.Length < 8) return false; + + return edid[0] == 0x00 && + edid[1] == 0xFF && + edid[2] == 0xFF && + edid[3] == 0xFF && + edid[4] == 0xFF && + edid[5] == 0xFF && + edid[6] == 0xFF && + edid[7] == 0x00; + } + + /// + /// Parses EDID version and revision from EDID. + /// + /// EDID data (at least 20 bytes) + /// EDID version information or null if invalid + public static EdidVersion? ParseEdidVersion(byte[] edid) + { + if (edid.Length < 20) return null; + + try + { + // EDID version is at bytes 18-19 + byte major = edid[18]; + byte minor = edid[19]; + return new EdidVersion(major, minor); + } + catch + { + return null; + } + } + + /// + /// Parses video input definition from EDID. + /// + /// EDID data (at least 21 bytes) + /// Video input definition or null if invalid + public static VideoInputDefinition? ParseVideoInputDefinition(byte[] edid) + { + if (edid.Length < 21) return null; + + try + { + // Video input definition is at byte 20 + byte value = edid[20]; + bool isDigital = (value & 0x80) != 0; // Bit 7 + return new VideoInputDefinition(isDigital, value); + } + catch + { + return null; + } + } + + /// + /// Parses supported features from EDID. + /// + /// EDID data (at least 25 bytes) + /// Supported features information or null if invalid + public static SupportedFeatures? ParseSupportedFeatures(byte[] edid) + { + if (edid.Length < 25) return null; + + try + { + // Supported features is at byte 24 + byte value = edid[24]; + + bool dpmsStandby = (value & 0x80) != 0; // Bit 7 + bool dpmsSuspend = (value & 0x40) != 0; // Bit 6 + bool dpmsActiveOff = (value & 0x20) != 0; // Bit 5 + byte displayType = (byte)((value >> 3) & 0x03); // Bits 4-3 + bool defaultColorSpace = (value & 0x04) != 0; // Bit 2 + bool preferredTimingMode = (value & 0x02) != 0; // Bit 1 + bool continuousFrequency = (value & 0x01) != 0; // Bit 0 + + return new SupportedFeatures( + dpmsStandby, + dpmsSuspend, + dpmsActiveOff, + displayType, + defaultColorSpace, + preferredTimingMode, + continuousFrequency, + value); + } + catch + { + return null; + } + } + + /// + /// Parses chromaticity coordinates (color points for red, green, blue, and white) from EDID. + /// + /// EDID data (at least 35 bytes) + /// Chromaticity coordinates or null if invalid + public static ChromaticityCoordinates? ParseChromaticity(byte[] edid) + { + if (edid.Length < 35) return null; + + try + { + // Chromaticity data is stored in bytes 25-34 + // Each coordinate is a 10-bit value split between two bytes + byte lsb = edid[25]; // Low-order bits for red/green/blue/white X + byte lsb2 = edid[26]; // Low-order bits for red/green/blue/white Y + + // Red X/Y: 8 MSB bits in byte 27, 2 LSB bits in bytes 25/26 + int redXRaw = (edid[27] << 2) | ((lsb >> 6) & 0x03); + int redYRaw = (edid[27] << 2) | ((lsb2 >> 6) & 0x03); + + // Green X/Y: 8 MSB bits in byte 28, 2 LSB bits in bytes 25/26 + int greenXRaw = (edid[28] << 2) | ((lsb >> 4) & 0x03); + int greenYRaw = (edid[28] << 2) | ((lsb2 >> 4) & 0x03); + + // Blue X/Y: 8 MSB bits in byte 29, 2 LSB bits in bytes 25/26 + int blueXRaw = (edid[29] << 2) | ((lsb >> 2) & 0x03); + int blueYRaw = (edid[29] << 2) | ((lsb2 >> 2) & 0x03); + + // White X/Y: 8 MSB bits in byte 30, 2 LSB bits in bytes 25/26 + int whiteXRaw = (edid[30] << 2) | (lsb & 0x03); + int whiteYRaw = (edid[30] << 2) | (lsb2 & 0x03); + + // Convert 10-bit values to 0.0-1.0 range + var red = new ColorPoint(redXRaw / 1024.0, redYRaw / 1024.0); + var green = new ColorPoint(greenXRaw / 1024.0, greenYRaw / 1024.0); + var blue = new ColorPoint(blueXRaw / 1024.0, blueYRaw / 1024.0); + var white = new ColorPoint(whiteXRaw / 1024.0, whiteYRaw / 1024.0); + + return new ChromaticityCoordinates(red, green, blue, white); + } + catch + { + return null; + } + } +} diff --git a/DDCSwitch/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/JsonContext.cs b/DDCSwitch/JsonContext.cs index 059cdf0..9ea4b3b 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -7,10 +7,16 @@ namespace DDCSwitch; [JsonSerializable(typeof(MonitorInfo))] [JsonSerializable(typeof(GetVcpResponse))] [JsonSerializable(typeof(SetVcpResponse))] +[JsonSerializable(typeof(ToggleInputResponse))] [JsonSerializable(typeof(VcpScanResponse))] [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, @@ -25,6 +31,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 @@ -37,7 +52,55 @@ 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); +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 00dab9a..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; @@ -16,9 +13,53 @@ 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; } + 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; + /// + /// 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); + EdidVersion = EdidParser.ParseEdidVersion(edid); + VideoInputDefinition = EdidParser.ParseVideoInputDefinition(edid); + SupportedFeatures = EdidParser.ParseSupportedFeatures(edid); + Chromaticity = EdidParser.ParseChromaticity(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..da45c42 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,89 @@ 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]; + } + + // No reliable EDID mapping found for this display index + return null; + } + catch (System.Security.SecurityException) + { + // No access to registry + return null; + } + catch (System.UnauthorizedAccessException) + { + // Access denied + return null; + } + catch (System.IO.IOException) + { + // Registry I/O error + return null; + } + catch (Exception) + { + // Unexpected error + return null; + } + } +} diff --git a/DDCSwitch/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; } diff --git a/EXAMPLES.md b/EXAMPLES.md index 1873c3d..dc77bd8 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,43 +1,144 @@ # 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. -## Comprehensive VCP Feature Examples +## Monitor Information (EDID) -ddcswitch now supports all MCCS (Monitor Control Command Set) standardized VCP features, organized by categories for easy discovery. +Retrieve detailed Extended Display Identification Data (EDID) from your monitors to view specifications, capabilities, and color characteristics. -### VCP Feature Categories +### Basic EDID Information -List all available feature categories: +View all EDID data for a specific monitor: ```powershell -ddcswitch list --categories +ddcswitch info 0 ``` -Output: +### JSON Output for EDID Data + +Retrieve EDID data in JSON format for programmatic access: + +```powershell +ddcswitch info 0 --json ``` -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 + +### 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 +} ``` -### Browse Features by Category +#### PowerShell: Log Monitor Information ```powershell -# Image adjustment features -ddcswitch list --category image +# Create monitor inventory with EDID details +$monitors = @() +$listOutput = ddcswitch list --json | ConvertFrom-Json -# Color control features -ddcswitch list --category color +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 + } +} -# Audio features -ddcswitch list --category audio +$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 + +ddcswitch now supports all MCCS (Monitor Control Command Set) standardized VCP features. + ### Color Control Examples Control RGB gains for color calibration: @@ -261,6 +362,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/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. - diff --git a/README.md b/README.md index 67844e0..b1cc3c2 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ 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.) - πŸ”† **Control brightness and contrast** with percentage values (0-100%) - πŸŽ›οΈ **Comprehensive VCP feature support** - Access all MCCS standardized monitor controls @@ -26,6 +27,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. @@ -89,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: @@ -138,17 +175,24 @@ ddcswitch set 0 HDMI1 ddcswitch set "LG ULTRAGEAR" HDMI2 ``` -Set brightness or contrast with percentage values: +### Toggle Between Input Sources + +Automatically switch between two input sources without specifying which one: ```powershell -# Set brightness to 75% -ddcswitch set 0 brightness 75% +# Toggle between HDMI1 and DisplayPort1 +ddcswitch toggle 0 HDMI1 DP1 -# Set contrast to 80% -ddcswitch set 0 contrast 80% +# Toggle by monitor name +ddcswitch toggle "LG ULTRAGEAR" HDMI1 HDMI2 ``` -Output: `βœ“ Successfully set brightness to 75% (120/160)` +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. ### Raw VCP Access @@ -252,6 +296,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%