From d4efada906958450ec7862da08873a5e42686dc0 Mon Sep 17 00:00:00 2001 From: Asger Hautop Drewsen Date: Thu, 12 Jan 2017 05:46:25 +0100 Subject: [PATCH] Only show attached monitors and use ddcctl directly --- .gitmodules | 3 + MonitorControl.OSX.xcodeproj/project.pbxproj | 33 +++ MonitorControl.OSX/AppDelegate.swift | 224 +++++++++++++------ MonitorControl.OSX/Bridging-Header.h | 2 + ddcctl | 1 + 5 files changed, 191 insertions(+), 72 deletions(-) create mode 100644 .gitmodules create mode 100644 MonitorControl.OSX/Bridging-Header.h create mode 160000 ddcctl diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8bfdbcf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ddcctl"] + path = ddcctl + url = https://github.com/kfix/ddcctl diff --git a/MonitorControl.OSX.xcodeproj/project.pbxproj b/MonitorControl.OSX.xcodeproj/project.pbxproj index 7a92928..10fbb12 100644 --- a/MonitorControl.OSX.xcodeproj/project.pbxproj +++ b/MonitorControl.OSX.xcodeproj/project.pbxproj @@ -7,12 +7,23 @@ objects = { /* Begin PBXBuildFile section */ + 55359E391E2737EC002671BC /* DDC.c in Sources */ = {isa = PBXBuildFile; fileRef = 55359E331E2737EC002671BC /* DDC.c */; }; + 55359E3B1E2737EC002671BC /* ddcctl.sh in Resources */ = {isa = PBXBuildFile; fileRef = 55359E361E2737EC002671BC /* ddcctl.sh */; }; + 55359E3C1E2737EC002671BC /* Makefile in Sources */ = {isa = PBXBuildFile; fileRef = 55359E371E2737EC002671BC /* Makefile */; }; + 55359E3D1E2737EC002671BC /* README.md in Sources */ = {isa = PBXBuildFile; fileRef = 55359E381E2737EC002671BC /* README.md */; }; 56754EAF1D9A4016007BCDC5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56754EAE1D9A4016007BCDC5 /* AppDelegate.swift */; }; 56754EB11D9A4016007BCDC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56754EB01D9A4016007BCDC5 /* Assets.xcassets */; }; 56754EB41D9A4016007BCDC5 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 56754EB21D9A4016007BCDC5 /* MainMenu.xib */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 55359E331E2737EC002671BC /* DDC.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = DDC.c; sourceTree = ""; }; + 55359E341E2737EC002671BC /* DDC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DDC.h; sourceTree = ""; }; + 55359E351E2737EC002671BC /* ddcctl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ddcctl.m; sourceTree = ""; }; + 55359E361E2737EC002671BC /* ddcctl.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = ddcctl.sh; sourceTree = ""; }; + 55359E371E2737EC002671BC /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; + 55359E381E2737EC002671BC /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 55359E3E1E27380B002671BC /* Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = ""; }; 56754EAB1D9A4016007BCDC5 /* MonitorControl.OSX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MonitorControl.OSX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 56754EAE1D9A4016007BCDC5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 56754EB01D9A4016007BCDC5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -31,10 +42,24 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 55359E321E2737EC002671BC /* ddcctl */ = { + isa = PBXGroup; + children = ( + 55359E331E2737EC002671BC /* DDC.c */, + 55359E341E2737EC002671BC /* DDC.h */, + 55359E351E2737EC002671BC /* ddcctl.m */, + 55359E361E2737EC002671BC /* ddcctl.sh */, + 55359E371E2737EC002671BC /* Makefile */, + 55359E381E2737EC002671BC /* README.md */, + ); + path = ddcctl; + sourceTree = ""; + }; 56754EA21D9A4016007BCDC5 = { isa = PBXGroup; children = ( 56754EAD1D9A4016007BCDC5 /* MonitorControl.OSX */, + 55359E321E2737EC002671BC /* ddcctl */, 56754EAC1D9A4016007BCDC5 /* Products */, ); sourceTree = ""; @@ -54,6 +79,7 @@ 56754EB01D9A4016007BCDC5 /* Assets.xcassets */, 56754EB21D9A4016007BCDC5 /* MainMenu.xib */, 56754EB51D9A4016007BCDC5 /* Info.plist */, + 55359E3E1E27380B002671BC /* Bridging-Header.h */, ); path = MonitorControl.OSX; sourceTree = ""; @@ -118,6 +144,7 @@ buildActionMask = 2147483647; files = ( 56754EB11D9A4016007BCDC5 /* Assets.xcassets in Resources */, + 55359E3B1E2737EC002671BC /* ddcctl.sh in Resources */, 56754EB41D9A4016007BCDC5 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -130,6 +157,9 @@ buildActionMask = 2147483647; files = ( 56754EAF1D9A4016007BCDC5 /* AppDelegate.swift in Sources */, + 55359E3D1E2737EC002671BC /* README.md in Sources */, + 55359E3C1E2737EC002671BC /* Makefile in Sources */, + 55359E391E2737EC002671BC /* DDC.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -246,6 +276,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "bluejamesbond.MonitorControl-OSX"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "MonitorControl.OSX/Bridging-Header.h"; SWIFT_VERSION = 3.0; }; name = Debug; @@ -259,6 +290,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "bluejamesbond.MonitorControl-OSX"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "MonitorControl.OSX/Bridging-Header.h"; SWIFT_VERSION = 3.0; }; name = Release; @@ -282,6 +314,7 @@ 56754EBA1D9A4016007BCDC5 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/MonitorControl.OSX/AppDelegate.swift b/MonitorControl.OSX/AppDelegate.swift index 01248c6..e17a9c3 100644 --- a/MonitorControl.OSX/AppDelegate.swift +++ b/MonitorControl.OSX/AppDelegate.swift @@ -9,6 +9,12 @@ import Cocoa import Foundation +struct Display { + var id: CGDirectDisplayID + var name: String + var serial: String +} + @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @@ -20,6 +26,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) let keycode = UInt16(0x07) + + var displays : [Display] = [] @IBAction func quitClicked(_ sender: AnyObject) { NSApplication.shared().terminate(self); @@ -28,117 +36,140 @@ class AppDelegate: NSObject, NSApplicationDelegate { func setBrightness( slider: NSSlider ){ let command = "-b"; let value = slider.integerValue; - let monitor = slider.tag; + let i = slider.tag; + let d = displays[i] - ddcctl(monitor: String(monitor), command: command, value: String(value)); + ddcctl(monitor: d.id, command: command, value: value); - prefs.setValue(value, forKey: "\(command)-\(monitor)"); + prefs.setValue(value, forKey: "\(command)-\(d.serial)"); prefs.synchronize(); } func setVolume(slider: NSSlider ){ let command = "-v"; let value = slider.integerValue; - let monitor = slider.tag; - - ddcctl(monitor: String(monitor), command: command, value: String(value)); - - prefs.setValue(value, forKey: "\(command)-\(monitor)"); + let i = slider.tag; + let d = displays[i] + + ddcctl(monitor: d.id, command: command, value: value); + + prefs.setValue(value, forKey: "\(command)-\(d.serial)"); prefs.synchronize(); } - + func setContrast(slider: NSSlider ){ let command = "-c"; let value = slider.integerValue; - let monitor = slider.tag; - - ddcctl(monitor: String(monitor), command: command, value: String(value)); - - prefs.setValue(value, forKey: "\(command)-\(monitor)"); + let i = slider.tag; + let d = displays[i] + + ddcctl(monitor: d.id, command: command, value: value); + + prefs.setValue(value, forKey: "\(command)-\(d.serial)"); prefs.synchronize(); } - + func applicationDidFinishLaunching(_ aNotification: Notification) { statusItem.title = "♨" - statusItem.menu = statusMenu; - - for i in (1...4).reversed() { + statusItem.menu = statusMenu + + var firstDisplay : Display? = nil + + for s in NSScreen.screens()! { + let id = s.deviceDescription["NSScreenNumber"] as! CGDirectDisplayID + if CGDisplayIsBuiltin(id) != 0 { + continue + } + + var edid = EDID() + if !EDIDTest(id, &edid) { + continue + } + + let name = getDisplayName(edid) + let serial = getDisplaySerial(edid) + + let d = Display(id: id, name: name, serial: serial) + displays.append(d) + + let i = displays.count - 1 + let monitorMenuItem = NSMenuItem(); let monitorSubMenu = NSMenu(); - + let brightnessItem = NSMenuItem(); let contrastItem = NSMenuItem(); let volumeItem = NSMenuItem(); let defaultMonitorItem = NSMenuItem(); let brightnessSlider = NSSlider(frame: NSRect(x: 20, y: 0, width: 200, height: 19)); - + brightnessSlider.target = self; brightnessSlider.minValue = 0; brightnessSlider.maxValue = 100; - brightnessSlider.integerValue = prefs.integer(forKey: "-b-\(i)") + brightnessSlider.integerValue = prefs.integer(forKey: "-b-\(serial)") brightnessSlider.action = #selector(AppDelegate.setBrightness); brightnessSlider.tag = i; - + let contrastSlider = NSSlider(frame: NSRect(x: 20, y: 0, width: 200, height: 19)); - + contrastSlider.target = self; contrastSlider.minValue = 0; contrastSlider.maxValue = 100; - contrastSlider.integerValue = prefs.integer(forKey: "-c-\(i)") + contrastSlider.integerValue = prefs.integer(forKey: "-c-\(serial)") contrastSlider.action = #selector(AppDelegate.setContrast); contrastSlider.tag = i; - + let volumeSlider = NSSlider(frame: NSRect(x: 20, y: 3, width: 200, height: 19)); - + volumeSlider.target = self; volumeSlider.minValue = 0; volumeSlider.maxValue = 100; - volumeSlider.integerValue = prefs.integer(forKey: "-v-\(i)") + volumeSlider.integerValue = prefs.integer(forKey: "-v-\(serial)") volumeSlider.action = #selector(AppDelegate.setVolume); volumeSlider.tag = i; - + let brightnesSliderView = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 40)); let contrastSliderView = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 40)); let volumeSliderView = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 40)); let defaultMonitorView = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 25)); - + let brightnessLabel = NSTextField(frame: NSRect(x: 20, y: 16, width: 130, height: 20)) brightnessLabel.stringValue = "Brightness"; brightnessLabel.isBordered = false; brightnessLabel.isBezeled = false; - + let brightnessLabelKeyCode = NSTextField(frame: NSRect(x: 120, y: 16, width: 100, height: 20)) brightnessLabelKeyCode.stringValue = "⇧⌘- / ⇧⌘+" brightnessLabelKeyCode.isBordered = false; brightnessLabelKeyCode.isBezeled = false; - brightnessLabelKeyCode.isHidden = i != 1; + brightnessLabelKeyCode.isHidden = firstDisplay == nil; brightnessLabelKeyCode.alignment = NSTextAlignment.right - + let constrastLabel = NSTextField(frame: NSRect(x: 20, y: 16, width: 130, height: 20)) constrastLabel.stringValue = "Contrast" constrastLabel.isBordered = false; constrastLabel.isBezeled = false; - + let volumeLabel = NSTextField(frame: NSRect(x: 20, y: 19, width: 130, height: 20)) volumeLabel.stringValue = "Volume" volumeLabel.isBordered = false; volumeLabel.isBezeled = false; - + let volumeLabelKeyCode = NSTextField(frame: NSRect(x: 120, y: 19, width: 100, height: 20)) volumeLabelKeyCode.stringValue = "⌥⌘- / ⌥⌘+" volumeLabelKeyCode.isBordered = false; volumeLabelKeyCode.isBezeled = false; - volumeLabelKeyCode.isHidden = i != 1; + volumeLabelKeyCode.isHidden = firstDisplay == nil; volumeLabelKeyCode.alignment = NSTextAlignment.right - + brightnesSliderView.addSubview(brightnessLabel) brightnesSliderView.addSubview(brightnessLabelKeyCode) brightnesSliderView.addSubview(brightnessSlider) contrastSliderView.addSubview(constrastLabel) contrastSliderView.addSubview(contrastSlider) - + volumeSliderView.addSubview(volumeLabel) volumeSliderView.addSubview(volumeLabelKeyCode) volumeSliderView.addSubview(volumeSlider) @@ -146,17 +177,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { brightnessItem.view = brightnesSliderView; contrastItem.view = contrastSliderView; volumeItem.view = volumeSliderView; - + let defaultMonitorSelectButtom = NSButton(frame: NSRect(x: 25, y: 0, width: 200, height: 25)); - defaultMonitorSelectButtom.title = i == 1 ? "Default" : "Set as default"; + defaultMonitorSelectButtom.title = firstDisplay == nil ? "Default" : "Set as default"; defaultMonitorSelectButtom.bezelStyle = NSRoundRectBezelStyle; - defaultMonitorSelectButtom.isEnabled = i != 1; + defaultMonitorSelectButtom.isEnabled = firstDisplay == nil; defaultMonitorSelectButtom.tag = i; - + defaultMonitorView.addSubview(defaultMonitorSelectButtom); - + defaultMonitorItem.view = defaultMonitorView; - + monitorSubMenu.addItem(brightnessItem); monitorSubMenu.addItem(NSMenuItem.separator()); monitorSubMenu.addItem(contrastItem); @@ -165,53 +196,59 @@ class AppDelegate: NSObject, NSApplicationDelegate { monitorSubMenu.addItem(NSMenuItem.separator()); monitorSubMenu.addItem(defaultMonitorItem); - monitorMenuItem.title = "Monitor \(i)"; + monitorMenuItem.title = "\(name)"; monitorMenuItem.submenu = monitorSubMenu; - + statusMenu.insertItem(monitorMenuItem, at: 0) + + if firstDisplay == nil { + firstDisplay = d + } } - + acquirePrivileges(); - + + if firstDisplay == nil { + return + } + + let d = firstDisplay! + NSEvent.addGlobalMonitorForEvents( matching: NSEventMask.keyDown, handler: {(event: NSEvent) in if (event.keyCode == 27 && (event.modifierFlags.contains(NSEventModifierFlags.control)) && (event.modifierFlags.contains(NSEventModifierFlags.command))) { - let monitor = 1; - let value = abs(self.prefs.integer(forKey: "-v-\(monitor)") - 1); - - self.prefs.setValue(value, forKey: "-v-\(monitor)"); - - self.ddcctl(monitor: String(monitor), command: "-v", value: String(value)); + let value = abs(self.prefs.integer(forKey: "-v-\(d.serial)") - 1); + + self.prefs.setValue(value, forKey: "-v-\(d.serial)"); + + self.ddcctl(monitor: d.id, command: "-v", value: value); } else if (event.keyCode == 24 && (event.modifierFlags.contains(NSEventModifierFlags.control)) && (event.modifierFlags.contains(NSEventModifierFlags.command))) { - let monitor = 1; - let value = abs(self.prefs.integer(forKey: "-v-\(monitor)") + 1); + let value = abs(self.prefs.integer(forKey: "-v-\(d.serial)") + 1); - self.prefs.setValue(value, forKey: "-v-\(monitor)"); + self.prefs.setValue(value, forKey: "-v-\(d.serial)"); - self.ddcctl(monitor: String(monitor), command: "-v", value: String(value)); + self.ddcctl(monitor: d.id, command: "-v", value: value); } else if (event.keyCode == 27 && (event.modifierFlags.contains(NSEventModifierFlags.option)) && (event.modifierFlags.contains(NSEventModifierFlags.command))) { - let monitor = 1; - let value = abs(self.prefs.integer(forKey: "-b-\(monitor)") - 1); + let value = abs(self.prefs.integer(forKey: "-b-\(d.serial)") - 1); - self.prefs.setValue(value, forKey: "-b-\(monitor)"); + self.prefs.setValue(value, forKey: "-b-\(d.serial))"); - self.ddcctl(monitor: String(monitor), command: "-b", value: String(value)); + self.ddcctl(monitor: d.id, command: "-b", value: value); } else if (event.keyCode == 24 && (event.modifierFlags.contains(NSEventModifierFlags.option)) && (event.modifierFlags.contains(NSEventModifierFlags.command))) { - let monitor = 1; - let value = abs(self.prefs.integer(forKey: "-b-\(monitor)") + 1); + let value = abs(self.prefs.integer(forKey: "-b-\(d.serial)") + 1); - self.prefs.setValue(value, forKey: "-b-\(monitor)"); + self.prefs.setValue(value, forKey: "-b-\(d.serial)"); - self.ddcctl(monitor: String(monitor), command: "-b", value: String(value)); + self.ddcctl(monitor: d.id, command: "-b", value: value); } }); } @@ -227,16 +264,59 @@ class AppDelegate: NSObject, NSApplicationDelegate { return; } - func ddcctl(monitor: String, command: String, value: String) { - let task = Process() - - task.launchPath = "/usr/local/bin/ddcctl" - task.arguments = ["-d", monitor, command, value] - task.launch() + func ddcctl(monitor: CGDirectDisplayID, command: String, value: Int) { + var cmd : Int32! = nil + switch command { + case "-b": + cmd = BRIGHTNESS + break + case "-v": + cmd = AUDIO_SPEAKER_VOLUME + break + case "-c": + cmd = CONTRAST + break + default: + precondition(false, "Unknown command: \(command)") + } + + var wrcmd = DDCWriteCommand(control_id: UInt8(cmd), new_value: UInt8(value)) + DDCWrite(monitor, &wrcmd) + print(value) } func applicationWillTerminate(_ aNotification: Notification) { // Insert code here to tear down your application } - + + func edidString(_ d: descriptor) -> String { + var s = "" + for (_, b) in Mirror(reflecting: d.text.data).children { + let b = b as! Int8 + let c = Character(UnicodeScalar(UInt8(bitPattern: b))) + if c == "\0" || c == "\n" { + break + } + s.append(c) + } + return s + } + + func getDescriptorString(_ edid: EDID, _ type: UInt8) -> String? { + for d in [edid.descriptor1, edid.descriptor2, edid.descriptor3, edid.descriptor4] { + if d.text.type == UInt8(type) { + return edidString(d) + } + } + + return nil + } + + func getDisplayName(_ edid: EDID) -> String { + return getDescriptorString(edid, 0xFC) ?? "Display" + } + + func getDisplaySerial(_ edid: EDID) -> String { + return getDescriptorString(edid, 0xFF) ?? "Unknown" + } } diff --git a/MonitorControl.OSX/Bridging-Header.h b/MonitorControl.OSX/Bridging-Header.h new file mode 100644 index 0000000..8c4e0fa --- /dev/null +++ b/MonitorControl.OSX/Bridging-Header.h @@ -0,0 +1,2 @@ +#import +#include "../ddcctl/DDC.h" diff --git a/ddcctl b/ddcctl new file mode 160000 index 0000000..dda64ce --- /dev/null +++ b/ddcctl @@ -0,0 +1 @@ +Subproject commit dda64ce43cdf0c16b2bedcf744033911a2bb88c5