package xorg import ( "context" "fmt" "slices" "edge-infra.dev/pkg/sds/display/displaymanager/applier" "edge-infra.dev/pkg/sds/display/displaymanager/applier/xorg/command" "edge-infra.dev/pkg/sds/display/displaymanager/applier/xorg/command/runner" "edge-infra.dev/pkg/sds/display/displaymanager/applier/xorg/command/xinput" "edge-infra.dev/pkg/sds/display/displaymanager/applier/xorg/command/xrandr" "edge-infra.dev/pkg/sds/display/displaymanager/applier/xorg/command/xset" v2 "edge-infra.dev/pkg/sds/display/k8s/apis/v2" "edge-infra.dev/pkg/sds/lib/xorg" ) // Returns a new DisplayApplier which can apply display configuration (represented // as a DisplayConfig) to the node by making calls to Xrandr, Xset and Xinput. // // Must be instantiated with xorg.OutputIDs and xorg.InputIDs to allow the // applier to identify devices read by the Xorg DisplayReader. func NewXorgDisplayApplier(outputIDs map[v2.DisplayPort]xorg.OutputID, inputIDs map[v2.InputDeviceName][]xorg.InputDeviceID, r runner.Runner) applier.DisplayApplier { return &displayApplier{ outputIDs: outputIDs, inputIDs: inputIDs, cmdRunner: r, } } type displayApplier struct { // outputIDs maps display-ports to output IDs, e.g. // {"card0-HDMI-A-1":"HDMI1","card0-DP-1":"DP1"} outputIDs map[v2.DisplayPort]xorg.OutputID // inputIDs maps input device names to each of their xinput IDs, e.g. // {"elo_touch_2077":["13","14"],"elo_touch_2088":["16"]} inputIDs map[v2.InputDeviceName][]xorg.InputDeviceID cmdRunner runner.Runner } func (x *displayApplier) Apply(_ context.Context, displayConfig *v2.DisplayConfig) (*v2.DisplayConfig, error) { if displayConfig == nil { return nil, nil } if err := validateOutputIDs(displayConfig, x.outputIDs); err != nil { return nil, err } filterInputDeviceMappings(displayConfig, x.inputIDs) cmds := []command.Command{ x.createXrandrCommand(displayConfig), x.createXsetCommand(displayConfig), x.createXinputCommand(displayConfig), } for _, cmd := range cmds { if err := cmd.Run(); err != nil { return nil, fmt.Errorf("failed to run %s: %w", cmd.Path(), err) } } return displayConfig, nil } // Validates each display in the DisplayConfig has an ID. func validateOutputIDs(displayConfig *v2.DisplayConfig, outputIDs map[v2.DisplayPort]xorg.OutputID) error { for _, dp := range displayConfig.DisplayPorts() { if outputID := outputIDs[dp]; outputID == "" { return fmt.Errorf("display %s does not have an output ID", dp) } } return nil } // Removes mappings from the display config where a matching input device does not exist, // or it was already mapped to another display. func filterInputDeviceMappings(displayConfig *v2.DisplayConfig, inputIDs map[v2.InputDeviceName][]xorg.InputDeviceID) { remainingInputDevices := []v2.InputDeviceName{} for inputDeviceName := range inputIDs { for range inputIDs[inputDeviceName] { remainingInputDevices = append(remainingInputDevices, inputDeviceName) } } for idx, display := range displayConfig.Displays { displayInputDevices := []v2.InputDeviceName{} // if the requested input device exists, add the mapping and remove it from the remaining devices for _, inputDevice := range display.InputDeviceMappings { if slices.Contains(remainingInputDevices, inputDevice) { displayInputDevices = append(displayInputDevices, inputDevice) remainingInputDevices = removeInputDevice(inputDevice, remainingInputDevices) } } // remove mapping slice if empty if len(displayInputDevices) == 0 { displayInputDevices = nil } display.InputDeviceMappings = displayInputDevices displayConfig.Displays[idx] = display } } // Removes the first instance of the target input device from inputDevices. func removeInputDevice(targetInputDevice v2.InputDeviceName, inputDevices []v2.InputDeviceName) []v2.InputDeviceName { for idx, inputDevice := range inputDevices { if inputDevice == targetInputDevice { return append(inputDevices[:idx], inputDevices[idx+1:]...) } } return inputDevices } func (x *displayApplier) createXrandrCommand(displayConfig *v2.DisplayConfig) command.Command { cmd := xrandr.NewXrandrCommand(x.cmdRunner) for _, display := range displayConfig.Displays { addDisplayToXrandrCommand(cmd, display, x.outputIDs[display.DisplayPort]) } addLayoutToXrandrCommand(cmd, displayConfig, x.outputIDs) return cmd } func addDisplayToXrandrCommand(cmd xrandr.Command, display v2.Display, outputID xorg.OutputID) { if display.IsPrimary() { cmd.SetOutputAsPrimary(outputID) } if display.Resolution != nil { cmd.SetOutputMode(outputID, display.Resolution.String()) } if display.Orientation != nil { cmd.SetOutputRotation(outputID, display.Orientation.String()) } } func addLayoutToXrandrCommand(cmd xrandr.Command, displayConfig *v2.DisplayConfig, outputIDs map[v2.DisplayPort]xorg.OutputID) { if len(displayConfig.Layout) == 0 { return } // create output ID layout layout := []xorg.OutputID{} for _, dp := range displayConfig.Layout { layout = append(layout, outputIDs[dp]) } // set first display to position 0x0 cmd.SetOutputPosition(layout[0], 0, 0) // set each remaining to be right-of the previous for i := 1; i < len(layout); i++ { outputID := layout[i] relativeTo := layout[i-1] cmd.SetOutputRightOf(outputID, relativeTo) } } func (x *displayApplier) createXsetCommand(displayConfig *v2.DisplayConfig) command.Command { cmd := xset.NewXsetCommand(x.cmdRunner) // do nothing if DPMS is not set if displayConfig.DPMS == nil { return cmd } if displayConfig.DPMS.BlankTime != nil { cmd.SetBlankTimeSeconds(*displayConfig.DPMS.BlankTime) } var standBySeconds, suspendTimeSeconds, offTimeSeconds int if displayConfig.DPMS.StandbyTime != nil { standBySeconds = *displayConfig.DPMS.StandbyTime } if displayConfig.DPMS.SuspendTime != nil { suspendTimeSeconds = *displayConfig.DPMS.SuspendTime } if displayConfig.DPMS.OffTime != nil { offTimeSeconds = *displayConfig.DPMS.OffTime } cmd.SetDPMSSettings(standBySeconds, suspendTimeSeconds, offTimeSeconds) // Set Enabled last as timeouts override it if displayConfig.DPMS.Enabled != nil { cmd.SetDPMS(*displayConfig.DPMS.Enabled) } return cmd } func (x *displayApplier) createXinputCommand(displayConfig *v2.DisplayConfig) command.Command { cmd := xinput.NewXinputCommand(x.cmdRunner) // track which device runtime IDs are available to assign to mappings inputDeviceAvailability := map[xorg.InputDeviceID]bool{} for inputDeviceName := range x.inputIDs { for _, inputID := range x.inputIDs[inputDeviceName] { inputDeviceAvailability[inputID] = true } } for _, display := range displayConfig.Displays { addDisplayToXinputCommand(cmd, display, x.outputIDs[display.DisplayPort], x.inputIDs, inputDeviceAvailability) } return cmd } func addDisplayToXinputCommand(cmd xinput.Command, display v2.Display, outputID xorg.OutputID, inputIDs map[v2.InputDeviceName][]xorg.InputDeviceID, inputDeviceAvailability map[xorg.InputDeviceID]bool) { // attempt to find an input device ID for each requested mapping, // adding the relevant map-to-output command if found for _, inputDeviceMapping := range display.InputDeviceMappings { if inputID := findAvailableInputDeviceWithName(inputDeviceMapping, inputIDs, inputDeviceAvailability); inputID != nil { cmd.MapToOutput(outputID, *inputID) return } } } // Attempts to find an unassigned device with the target input device name, // returning nil if not found or already assigned. func findAvailableInputDeviceWithName(targetName v2.InputDeviceName, inputIDs map[v2.InputDeviceName][]xorg.InputDeviceID, inputDeviceAvailability map[xorg.InputDeviceID]bool) *xorg.InputDeviceID { // iterate over the possible device IDs for the target name for _, inputID := range inputIDs[targetName] { // if we find an available ID, set as unavailable and return it if inputDeviceAvailability[inputID] { inputDeviceAvailability[inputID] = false return &inputID } } return nil }