♻️ Refactoring
- Key handling is now done in the app, close #3 - All code linted with SwiftLint, close #4 - Better handling of built-in screen Signed-off-by: Guillaume Broder <iamnotheoneyouseek@gmail.com>
45
.gitignore
vendored
|
|
@ -1,5 +1,40 @@
|
|||
|
||||
# Created by https://www.gitignore.io/api/xcode
|
||||
# Created by https://www.gitignore.io/api/macos,xcode,cocoapods
|
||||
|
||||
### CocoaPods ###
|
||||
## CocoaPods GitIgnore Template
|
||||
|
||||
# CocoaPods - Only use to conserve bandwidth / Save time on Pushing
|
||||
# - Also handy if you have a large number of dependant pods
|
||||
# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE
|
||||
Pods/
|
||||
|
||||
### macOS ###
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
|
|
@ -25,4 +60,10 @@ xcuserdata/
|
|||
*.moved-aside
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
*.xcscheme
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
||||
|
|
|
|||
9
.swiftlint.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
disabled_rules:
|
||||
- line_length
|
||||
- function_body_length
|
||||
excluded:
|
||||
- Pods
|
||||
type_body_length:
|
||||
- warning: 500
|
||||
file_length:
|
||||
- warning: 500
|
||||
|
|
@ -12,6 +12,8 @@
|
|||
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 */; };
|
||||
9A19D3B73485870616B6D4E0 /* Pods_MonitorControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 398F482D5C8816B29F16AAEB /* Pods_MonitorControl.framework */; };
|
||||
F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A8DF11FFBAA6F0034DC27 /* Display.swift */; };
|
||||
F091C9B31F6EA6110096FD65 /* SliderHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F091C9B21F6EA6110096FD65 /* SliderHandler.swift */; };
|
||||
F091C9B81F6EA79B0096FD65 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F091C9B71F6EA79B0096FD65 /* Utils.swift */; };
|
||||
F0A987E81F77B40E009B603D /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0A987D61F77B290009B603D /* OSD.framework */; };
|
||||
|
|
@ -19,6 +21,9 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
31E16D90527EBD3F8A12BE0B /* Pods-MonitorControl.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonitorControl.release.xcconfig"; path = "Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl.release.xcconfig"; sourceTree = "<group>"; };
|
||||
398F482D5C8816B29F16AAEB /* Pods_MonitorControl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MonitorControl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
42B61ABC1D7907131330228A /* Pods-MonitorControl.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonitorControl.debug.xcconfig"; path = "Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
55359E331E2737EC002671BC /* DDC.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = DDC.c; sourceTree = "<group>"; };
|
||||
55359E341E2737EC002671BC /* DDC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DDC.h; sourceTree = "<group>"; };
|
||||
55359E351E2737EC002671BC /* ddcctl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ddcctl.m; sourceTree = "<group>"; };
|
||||
|
|
@ -30,6 +35,7 @@
|
|||
56754EB01D9A4016007BCDC5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
56754EB31D9A4016007BCDC5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
56754EB51D9A4016007BCDC5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F03A8DF11FFBAA6F0034DC27 /* Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = "<group>"; };
|
||||
F091C9B21F6EA6110096FD65 /* SliderHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderHandler.swift; sourceTree = "<group>"; };
|
||||
F091C9B71F6EA79B0096FD65 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
|
||||
F091C9B91F6EB43B0096FD65 /* fr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = fr; path = fr.lproj/MainMenu.strings; sourceTree = "<group>"; };
|
||||
|
|
@ -57,6 +63,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F0A987E81F77B40E009B603D /* OSD.framework in Frameworks */,
|
||||
9A19D3B73485870616B6D4E0 /* Pods_MonitorControl.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -84,6 +91,7 @@
|
|||
55359E321E2737EC002671BC /* ddcctl */,
|
||||
56754EAC1D9A4016007BCDC5 /* Products */,
|
||||
F0A987D71F77B404009B603D /* Frameworks */,
|
||||
EFFC2F3E35BEC9ACFA754137 /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -110,10 +118,20 @@
|
|||
path = MonitorControl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EFFC2F3E35BEC9ACFA754137 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
42B61ABC1D7907131330228A /* Pods-MonitorControl.debug.xcconfig */,
|
||||
31E16D90527EBD3F8A12BE0B /* Pods-MonitorControl.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F091C9B41F6EA6180096FD65 /* Objects */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F091C9B21F6EA6110096FD65 /* SliderHandler.swift */,
|
||||
F03A8DF11FFBAA6F0034DC27 /* Display.swift */,
|
||||
);
|
||||
path = Objects;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -122,6 +140,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
F0A987D81F77B404009B603D /* MonitorControl */,
|
||||
398F482D5C8816B29F16AAEB /* Pods_MonitorControl.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -157,9 +176,13 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 56754EB81D9A4016007BCDC5 /* Build configuration list for PBXNativeTarget "MonitorControl" */;
|
||||
buildPhases = (
|
||||
C0EF20D28FC7408CBE89A686 /* [CP] Check Pods Manifest.lock */,
|
||||
F03A8DF01FFB9D4C0034DC27 /* [Lint] Run SwiftLint */,
|
||||
56754EA71D9A4016007BCDC5 /* Sources */,
|
||||
56754EA81D9A4016007BCDC5 /* Frameworks */,
|
||||
56754EA91D9A4016007BCDC5 /* Resources */,
|
||||
9DD5968596EFAF0E2EB56496 /* [CP] Embed Pods Frameworks */,
|
||||
3ACE91333FC1E781FB2E44E5 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -182,6 +205,7 @@
|
|||
TargetAttributes = {
|
||||
56754EAA1D9A4016007BCDC5 = {
|
||||
CreatedOnToolsVersion = 8.0;
|
||||
DevelopmentTeam = KGY56RWR9A;
|
||||
LastSwiftMigration = 0900;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
|
|
@ -220,11 +244,80 @@
|
|||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3ACE91333FC1E781FB2E44E5 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9DD5968596EFAF0E2EB56496 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${SRCROOT}/Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl-frameworks.sh",
|
||||
"${BUILT_PRODUCTS_DIR}/MediaKeyTap/MediaKeyTap.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MediaKeyTap.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-MonitorControl/Pods-MonitorControl-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C0EF20D28FC7408CBE89A686 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-MonitorControl-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F03A8DF01FFB9D4C0034DC27 /* [Lint] Run SwiftLint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[Lint] Run SwiftLint";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
56754EA71D9A4016007BCDC5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F03A8DF21FFBAA6F0034DC27 /* Display.swift in Sources */,
|
||||
F091C9B31F6EA6110096FD65 /* SliderHandler.swift in Sources */,
|
||||
56754EAF1D9A4016007BCDC5 /* AppDelegate.swift in Sources */,
|
||||
55359E391E2737EC002671BC /* DDC.c in Sources */,
|
||||
|
|
@ -393,13 +486,13 @@
|
|||
};
|
||||
56754EB91D9A4016007BCDC5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 42B61ABC1D7907131330228A /* Pods-MonitorControl.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/**";
|
||||
DEVELOPMENT_TEAM = KGY56RWR9A;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/MonitorControl/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl;
|
||||
|
|
@ -412,13 +505,13 @@
|
|||
};
|
||||
56754EBA1D9A4016007BCDC5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 31E16D90527EBD3F8A12BE0B /* Pods-MonitorControl.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/**";
|
||||
DEVELOPMENT_TEAM = KGY56RWR9A;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/MonitorControl/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl;
|
||||
|
|
|
|||
10
MonitorControl.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:MonitorControl.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -9,227 +9,130 @@
|
|||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
struct Display {
|
||||
var id: CGDirectDisplayID
|
||||
var name: String
|
||||
var serial: String
|
||||
}
|
||||
import MediaKeyTap
|
||||
|
||||
var app: AppDelegate! = nil
|
||||
let prefs = UserDefaults.standard
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate {
|
||||
|
||||
@IBOutlet weak var statusMenu: NSMenu!
|
||||
@IBOutlet weak var window: NSWindow!
|
||||
|
||||
|
||||
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
|
||||
var monitorItems: [NSMenuItem] = []
|
||||
var displays: [Display] = []
|
||||
var sliderHandlers: [SliderHandler] = []
|
||||
var sliderHandlers: [SliderHandler] = []
|
||||
|
||||
var defaultDisplay: Display! = nil
|
||||
var defaultBrightnessSlider: NSSlider! = nil
|
||||
var defaultVolumeSlider: NSSlider! = nil
|
||||
|
||||
let step = 100/16;
|
||||
let step = 100/16
|
||||
|
||||
@IBAction func quitClicked(_ sender: AnyObject) {
|
||||
NSApplication.shared.terminate(self)
|
||||
}
|
||||
var mediaKeyTap: MediaKeyTap?
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
app = self
|
||||
mediaKeyTap = MediaKeyTap.init(delegate: self, forKeys: [.brightnessUp, .brightnessDown, .mute, .volumeUp, .volumeDown], observeBuiltIn: false)
|
||||
|
||||
statusItem.image = NSImage.init(named: NSImage.Name(rawValue: "status"))
|
||||
statusItem.menu = statusMenu
|
||||
|
||||
acquirePrivileges()
|
||||
Utils.acquirePrivileges()
|
||||
|
||||
CGDisplayRegisterReconfigurationCallback({_,_,_ in app.updateDisplays()}, nil)
|
||||
CGDisplayRegisterReconfigurationCallback({_, _, _ in app.updateDisplays()}, nil)
|
||||
updateDisplays()
|
||||
|
||||
NSEvent.addGlobalMonitorForEvents(
|
||||
matching: NSEvent.EventTypeMask.keyDown, handler: {(event: NSEvent) in
|
||||
if self.defaultDisplay == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Keyboard shortcut only for main screen
|
||||
let currentDisplayId = NSScreen.main?.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as! CGDirectDisplayID
|
||||
if (self.defaultDisplay.id != currentDisplayId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Brightness -> Shift + Control + Alt + Command + (Up/Down)
|
||||
// Volume -> Shift + Control + Alt + Command + (Left/Right)
|
||||
// Mute -> Minus
|
||||
|
||||
// Capture keys
|
||||
let modifiers = NSEvent.ModifierFlags.init(rawValue: NSEvent.ModifierFlags.shift.rawValue | NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.control.rawValue | NSEvent.ModifierFlags.option.rawValue)
|
||||
let flags = event.modifierFlags.intersection(modifiers)
|
||||
|
||||
// Only do something if all modifiers are active
|
||||
if !flags.contains(NSEvent.ModifierFlags.shift) || !flags.contains(NSEvent.ModifierFlags.command) || !flags.contains(NSEvent.ModifierFlags.control) || !flags.contains(NSEvent.ModifierFlags.option) {
|
||||
return
|
||||
}
|
||||
|
||||
var brightnessRel = 0
|
||||
var volumeRel = 0
|
||||
var rel = 0
|
||||
|
||||
// Down key
|
||||
if event.keyCode == Utils.key.keyDownArrow.rawValue {
|
||||
brightnessRel = -self.step
|
||||
// Up key
|
||||
} else if event.keyCode == Utils.key.keyUpArrow.rawValue {
|
||||
brightnessRel = +self.step
|
||||
// Left key
|
||||
} else if event.keyCode == Utils.key.keyLeftArrow.rawValue {
|
||||
volumeRel = -self.step
|
||||
// Right key
|
||||
} else if event.keyCode == Utils.key.keyRightArrow.rawValue {
|
||||
volumeRel = +self.step
|
||||
// M key
|
||||
} else if event.keyCode == Utils.key.keyMute.rawValue {
|
||||
volumeRel = -100
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
var command = Int32()
|
||||
var slider: NSSlider! = nil
|
||||
if brightnessRel == 0 {
|
||||
command = AUDIO_SPEAKER_VOLUME
|
||||
slider = self.defaultVolumeSlider
|
||||
rel = volumeRel
|
||||
} else if volumeRel == 0 {
|
||||
command = BRIGHTNESS
|
||||
slider = self.defaultBrightnessSlider
|
||||
rel = brightnessRel
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
let k = "\(command)-\(self.defaultDisplay.serial)"
|
||||
let value = max(0, min(100, prefs.integer(forKey: k) + rel))
|
||||
|
||||
prefs.setValue(value, forKey: k)
|
||||
prefs.synchronize()
|
||||
slider.intValue = Int32(value)
|
||||
|
||||
Utils.ddcctl(monitor: self.defaultDisplay.id, command: command, value: value)
|
||||
|
||||
// OSD
|
||||
let manager : OSDManager = OSDManager.sharedManager() as! OSDManager
|
||||
var osdImage : Int = 1 // Brightness Image
|
||||
if brightnessRel == 0 {
|
||||
osdImage = 3 // Speaker image
|
||||
if value == 0 {
|
||||
osdImage = 4 // Mute speaker
|
||||
}
|
||||
}
|
||||
manager.showImage(Int64(osdImage), onDisplayID: self.defaultDisplay.id, priority: 0x1f4, msecUntilFade: 2000, filledChiclets: UInt32(value/self.step), totalChiclets: UInt32(100/self.step), locked: false)
|
||||
})
|
||||
mediaKeyTap?.start()
|
||||
}
|
||||
|
||||
func addSliderItem(menu: NSMenu, isDefaultDisplay: Bool, display: Display, command: Int32, title: String) -> NSSlider {
|
||||
let item = NSMenuItem()
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
}
|
||||
|
||||
let view = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 40))
|
||||
@IBAction func quitClicked(_ sender: AnyObject) {
|
||||
NSApplication.shared.terminate(self)
|
||||
}
|
||||
|
||||
let label = Utils.makeLabel(text: title, frame: NSRect(x: 20, y: 19, width: 130, height: 20))
|
||||
// MARK: - Menu
|
||||
|
||||
let handler = SliderHandler(display: display, command: command)
|
||||
sliderHandlers.append(handler)
|
||||
func clearDisplays() {
|
||||
defaultDisplay = nil
|
||||
defaultBrightnessSlider = nil
|
||||
defaultVolumeSlider = nil
|
||||
|
||||
let slider = NSSlider(frame: NSRect(x: 20, y: 0, width: 200, height: 19))
|
||||
slider.target = handler
|
||||
slider.minValue = 0
|
||||
slider.maxValue = 100
|
||||
slider.integerValue = prefs.integer(forKey: "\(command)-\(display.serial)")
|
||||
slider.action = #selector(SliderHandler.valueChanged)
|
||||
for monitor in monitorItems {
|
||||
statusMenu.removeItem(monitor)
|
||||
}
|
||||
|
||||
view.addSubview(label)
|
||||
view.addSubview(slider)
|
||||
|
||||
item.view = view
|
||||
|
||||
menu.addItem(item)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
return slider
|
||||
}
|
||||
monitorItems = []
|
||||
displays = []
|
||||
sliderHandlers = []
|
||||
}
|
||||
|
||||
func updateDisplays() {
|
||||
defaultDisplay = nil
|
||||
defaultBrightnessSlider = nil
|
||||
defaultVolumeSlider = nil
|
||||
|
||||
for m in monitorItems {
|
||||
statusMenu.removeItem(m)
|
||||
}
|
||||
|
||||
monitorItems = []
|
||||
displays = []
|
||||
sliderHandlers = []
|
||||
|
||||
clearDisplays()
|
||||
sleep(1)
|
||||
|
||||
for s in NSScreen.screens {
|
||||
let id = s.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as! CGDirectDisplayID
|
||||
if CGDisplayIsBuiltin(id) != 0 {
|
||||
continue
|
||||
}
|
||||
for screen in NSScreen.screens {
|
||||
if let id = screen.deviceDescription[NSDeviceDescriptionKey.init("NSScreenNumber")] as? CGDirectDisplayID {
|
||||
// Is Built In Screen (e.g. MBP/iMac Screen)
|
||||
if CGDisplayIsBuiltin(id) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var edid = EDID()
|
||||
if !EDIDTest(id, &edid) {
|
||||
continue
|
||||
}
|
||||
// Does screen support EDID ?
|
||||
var edid = EDID()
|
||||
if !EDIDTest(id, &edid) {
|
||||
continue
|
||||
}
|
||||
|
||||
let name = getDisplayName(edid)
|
||||
let serial = getDisplaySerial(edid)
|
||||
let name = Utils.getDisplayName(forEdid: edid)
|
||||
let serial = Utils.getDisplaySerial(forEdid: edid)
|
||||
|
||||
let isDefaultDisplay = defaultDisplay == nil
|
||||
let display = Display(identifier: id, name: name, serial: serial, isBuiltIn: false)
|
||||
displays.append(display)
|
||||
|
||||
let d = Display(id: id, name: name, serial: serial)
|
||||
displays.append(d)
|
||||
let monitorSubMenu = NSMenu()
|
||||
let brightnessSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu,
|
||||
forDisplay: display,
|
||||
command: BRIGHTNESS,
|
||||
title: NSLocalizedString("Brightness", comment: "Shown in menu"))
|
||||
let volumeSliderHandler = Utils.addSliderMenuItem(toMenu: monitorSubMenu,
|
||||
forDisplay: display,
|
||||
command: AUDIO_SPEAKER_VOLUME,
|
||||
title: NSLocalizedString("Volume", comment: "Shown in menu"))
|
||||
sliderHandlers.append(brightnessSliderHandler)
|
||||
sliderHandlers.append(volumeSliderHandler)
|
||||
|
||||
let monitorMenuItem = NSMenuItem()
|
||||
let monitorSubMenu = NSMenu()
|
||||
let isDefaultDisplay = defaultDisplay == nil
|
||||
let defaultMonitorSelectButtom = NSButton(frame: NSRect(x: 25, y: 0, width: 200, height: 25))
|
||||
defaultMonitorSelectButtom.title = isDefaultDisplay ? NSLocalizedString("Default", comment: "Shown in menu") : NSLocalizedString("Set as default", comment: "Shown in menu")
|
||||
defaultMonitorSelectButtom.bezelStyle = NSButton.BezelStyle.rounded
|
||||
defaultMonitorSelectButtom.isEnabled = !isDefaultDisplay
|
||||
|
||||
let brightnessSlider = addSliderItem(menu: monitorSubMenu, isDefaultDisplay: isDefaultDisplay, display: d, command: BRIGHTNESS, title: NSLocalizedString("Brightness", comment: "Sown in menu"))
|
||||
let _ = addSliderItem(menu: monitorSubMenu, isDefaultDisplay: isDefaultDisplay, display: d, command: CONTRAST, title: NSLocalizedString("Contrast", comment: "Shown in menu"))
|
||||
let volumeSlider = addSliderItem(menu: monitorSubMenu, isDefaultDisplay: isDefaultDisplay, display: d, command: AUDIO_SPEAKER_VOLUME, title: NSLocalizedString("Volume", comment: "Shown in menu"))
|
||||
let defaultMonitorView = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 25))
|
||||
defaultMonitorView.addSubview(defaultMonitorSelectButtom)
|
||||
|
||||
let defaultMonitorItem = NSMenuItem()
|
||||
let defaultMonitorView = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 25))
|
||||
let defaultMonitorItem = NSMenuItem()
|
||||
defaultMonitorItem.view = defaultMonitorView
|
||||
monitorSubMenu.addItem(defaultMonitorItem)
|
||||
|
||||
let defaultMonitorSelectButtom = NSButton(frame: NSRect(x: 25, y: 0, width: 200, height: 25))
|
||||
defaultMonitorSelectButtom.title = isDefaultDisplay ? NSLocalizedString("Default", comment: "Shown in menu") : NSLocalizedString("Set as default", comment: "Shown in menu")
|
||||
defaultMonitorSelectButtom.bezelStyle = NSButton.BezelStyle.rounded
|
||||
defaultMonitorSelectButtom.isEnabled = !isDefaultDisplay
|
||||
let monitorMenuItem = NSMenuItem()
|
||||
monitorMenuItem.title = "\(name)"
|
||||
monitorMenuItem.submenu = monitorSubMenu
|
||||
|
||||
defaultMonitorView.addSubview(defaultMonitorSelectButtom)
|
||||
monitorItems.append(monitorMenuItem)
|
||||
statusMenu.insertItem(monitorMenuItem, at: displays.count - 1)
|
||||
|
||||
defaultMonitorItem.view = defaultMonitorView
|
||||
|
||||
monitorSubMenu.addItem(defaultMonitorItem)
|
||||
|
||||
monitorMenuItem.title = "\(name)"
|
||||
monitorMenuItem.submenu = monitorSubMenu
|
||||
|
||||
monitorItems.append(monitorMenuItem)
|
||||
statusMenu.insertItem(monitorMenuItem, at: displays.count - 1)
|
||||
|
||||
if isDefaultDisplay {
|
||||
defaultDisplay = d
|
||||
defaultBrightnessSlider = brightnessSlider
|
||||
defaultVolumeSlider = volumeSlider
|
||||
}
|
||||
if isDefaultDisplay {
|
||||
defaultDisplay = display
|
||||
defaultBrightnessSlider = brightnessSliderHandler.slider
|
||||
defaultVolumeSlider = volumeSliderHandler.slider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if defaultDisplay == nil {
|
||||
|
|
@ -241,51 +144,64 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
statusMenu.insertItem(item, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
func acquirePrivileges() {
|
||||
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
|
||||
let accessibilityEnabled = AXIsProcessTrustedWithOptions(options)
|
||||
|
||||
if !accessibilityEnabled {
|
||||
print(NSLocalizedString("You need to enable the keylogger in the System Prefrences for the keyboard shortcuts to work", comment: ""))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// MARK: - Media Key Tap delegate
|
||||
|
||||
func getDescriptorString(_ edid: EDID, _ type: UInt8) -> String? {
|
||||
for (_, d) in Mirror(reflecting: edid.descriptors).children {
|
||||
let d = d as! descriptor
|
||||
if d.text.type == UInt8(type) {
|
||||
return edidString(d)
|
||||
}
|
||||
}
|
||||
func handle(mediaKey: MediaKey, event: KeyEvent) {
|
||||
|
||||
return nil
|
||||
}
|
||||
var command = BRIGHTNESS
|
||||
var rel = 0
|
||||
var slider = self.defaultBrightnessSlider
|
||||
|
||||
func getDisplayName(_ edid: EDID) -> String {
|
||||
return getDescriptorString(edid, 0xFC) ?? NSLocalizedString("Display", comment: "")
|
||||
}
|
||||
switch mediaKey {
|
||||
case .brightnessUp:
|
||||
rel = +self.step
|
||||
case .brightnessDown:
|
||||
rel = -self.step
|
||||
case .mute:
|
||||
rel = -100
|
||||
command = AUDIO_SPEAKER_VOLUME
|
||||
slider = self.defaultVolumeSlider
|
||||
case .volumeUp:
|
||||
rel = +self.step
|
||||
command = AUDIO_SPEAKER_VOLUME
|
||||
slider = self.defaultVolumeSlider
|
||||
case .volumeDown:
|
||||
rel = -self.step
|
||||
command = AUDIO_SPEAKER_VOLUME
|
||||
slider = self.defaultVolumeSlider
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
let k = "\(command)-\(self.defaultDisplay.serial)"
|
||||
let value = max(0, min(100, prefs.integer(forKey: k) + rel))
|
||||
prefs.setValue(value, forKey: k)
|
||||
prefs.synchronize()
|
||||
|
||||
if let slider = slider {
|
||||
slider.intValue = Int32(value)
|
||||
}
|
||||
|
||||
Utils.ddcctl(monitor: self.defaultDisplay.identifier, command: command, value: value)
|
||||
|
||||
// OSD
|
||||
if let manager = OSDManager.sharedManager() as? OSDManager {
|
||||
var osdImage: Int64 = 1 // Brightness Image
|
||||
if command == AUDIO_SPEAKER_VOLUME {
|
||||
osdImage = 3 // Speaker image
|
||||
if value == 0 {
|
||||
osdImage = 4 // Mute speaker
|
||||
}
|
||||
}
|
||||
manager.showImage(osdImage,
|
||||
onDisplayID: self.defaultDisplay.identifier,
|
||||
priority: 0x1f4,
|
||||
msecUntilFade: 2000,
|
||||
filledChiclets: UInt32(value/self.step),
|
||||
totalChiclets: UInt32(100/self.step),
|
||||
locked: false)
|
||||
}
|
||||
}
|
||||
|
||||
func getDisplaySerial(_ edid: EDID) -> String {
|
||||
return getDescriptorString(edid, 0xFF) ?? NSLocalizedString("Unknown", comment: "")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,63 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-33.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-257.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-513.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
Normal file
|
After Width: | Height: | Size: 986 KiB |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-128.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-16.png
Normal file
|
After Width: | Height: | Size: 754 B |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-256.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-257.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-32.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-33.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-512.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-513.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
MonitorControl/Assets.xcassets/AppIcon.appiconset/Icon-64.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
|
|
@ -17,9 +17,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>20</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
|
|
|||
17
MonitorControl/Objects/Display.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Display.swift
|
||||
// MonitorControl
|
||||
//
|
||||
// Created by Guillaume BRODER on 02/01/2018.
|
||||
// Copyright © 2018 Mathew Kurian. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
/// A display
|
||||
struct Display {
|
||||
var identifier: CGDirectDisplayID
|
||||
var name: String
|
||||
var serial: String
|
||||
var isBuiltIn: Bool = false
|
||||
}
|
||||
|
|
@ -8,29 +8,31 @@
|
|||
|
||||
import Cocoa
|
||||
|
||||
class SliderHandler: NSObject {
|
||||
var display : Display
|
||||
var command : Int32 = 0
|
||||
|
||||
/// Handle the slider
|
||||
class SliderHandler {
|
||||
var slider: NSSlider?
|
||||
var display: Display
|
||||
var command: Int32 = 0
|
||||
|
||||
public init(display: Display, command: Int32) {
|
||||
self.display = display
|
||||
self.command = command
|
||||
}
|
||||
|
||||
|
||||
@objc func valueChanged(slider: NSSlider) {
|
||||
let snapInterval = 25
|
||||
let snapThreshold = 3
|
||||
|
||||
|
||||
var value = slider.integerValue
|
||||
|
||||
|
||||
let closest = (value + snapInterval / 2) / snapInterval * snapInterval
|
||||
if abs(closest - value) <= snapThreshold {
|
||||
value = closest
|
||||
slider.integerValue = value
|
||||
}
|
||||
|
||||
Utils.ddcctl(monitor: display.id, command: command, value: value)
|
||||
|
||||
|
||||
Utils.ddcctl(monitor: display.identifier, command: command, value: value)
|
||||
|
||||
prefs.setValue(value, forKey: "\(command)-\(display.serial)")
|
||||
prefs.synchronize()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
import Cocoa
|
||||
|
||||
class Utils: NSObject {
|
||||
|
||||
|
||||
// MARK: - DDCCTL
|
||||
|
||||
/// Send command to ddcctl
|
||||
///
|
||||
/// - Parameters:
|
||||
|
|
@ -21,7 +23,9 @@ class Utils: NSObject {
|
|||
DDCWrite(monitor, &wrcmd)
|
||||
print(value)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
/// Create a label
|
||||
///
|
||||
/// - Parameters:
|
||||
|
|
@ -38,18 +42,109 @@ class Utils: NSObject {
|
|||
return label
|
||||
}
|
||||
|
||||
|
||||
/// Enum for hardware independent keyCode
|
||||
/// Create a slider and add it to the menu
|
||||
///
|
||||
/// - keyLeftArrow: keyCode for the left arrow
|
||||
/// - keyRightArrow: keyCode for the right arrow
|
||||
/// - keyDownArrow: keyCode for the down arrow
|
||||
/// - keyUpArrow: keyCode for the up arrow
|
||||
enum key : Int {
|
||||
case keyLeftArrow = 123
|
||||
case keyRightArrow = 124
|
||||
case keyDownArrow = 125
|
||||
case keyUpArrow = 126
|
||||
case keyMute = 24
|
||||
/// - Parameters:
|
||||
/// - menu: Menu containing the slider
|
||||
/// - display: Display to control
|
||||
/// - command: Command (Brightness/Volume/...)
|
||||
/// - title: Title of the slider
|
||||
/// - Returns: An `NSSlider` slider
|
||||
static func addSliderMenuItem(toMenu menu: NSMenu, forDisplay display: Display, command: Int32, title: String) -> SliderHandler {
|
||||
let item = NSMenuItem()
|
||||
let view = NSView(frame: NSRect(x: 0, y: 5, width: 250, height: 40))
|
||||
let label = Utils.makeLabel(text: title, frame: NSRect(x: 20, y: 19, width: 130, height: 20))
|
||||
let handler = SliderHandler(display: display, command: command)
|
||||
let slider = NSSlider(frame: NSRect(x: 20, y: 0, width: 200, height: 19))
|
||||
slider.target = handler
|
||||
slider.minValue = 0
|
||||
slider.maxValue = 100
|
||||
slider.integerValue = prefs.integer(forKey: "\(command)-\(display.serial)")
|
||||
slider.action = #selector(SliderHandler.valueChanged)
|
||||
handler.slider = slider
|
||||
|
||||
view.addSubview(label)
|
||||
view.addSubview(slider)
|
||||
|
||||
item.view = view
|
||||
|
||||
menu.addItem(item)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// MARK: - Utilities
|
||||
|
||||
/// Acquire Privileges (Necessary to listen to keyboard event globally)
|
||||
static func acquirePrivileges() {
|
||||
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
|
||||
let accessibilityEnabled = AXIsProcessTrustedWithOptions(options)
|
||||
|
||||
if !accessibilityEnabled {
|
||||
let alert = NSAlert()
|
||||
alert.addButton(withTitle: NSLocalizedString("Ok", comment: "Shown in the alert dialog"))
|
||||
alert.messageText = NSLocalizedString("Shortcuts not available", comment: "Shown in the alert dialog")
|
||||
alert.informativeText = NSLocalizedString("You need to enable MonitorControl in System Preferences > Security and Privacy > Accessibility for the keyboard shortcuts to work", comment: "Shown in the alert dialog")
|
||||
alert.alertStyle = .warning
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MARK: - Display Infos
|
||||
|
||||
/// Get the descriptor text
|
||||
///
|
||||
/// - Parameter descriptor: the descriptor
|
||||
/// - Returns: a string
|
||||
static func getEdidString(_ descriptor: descriptor) -> String {
|
||||
var result = ""
|
||||
for (_, bitChar) in Mirror(reflecting: descriptor.text.data).children {
|
||||
if let bitChar = bitChar as? Int8 {
|
||||
let char = Character(UnicodeScalar(UInt8(bitPattern: bitChar)))
|
||||
if char == "\0" || char == "\n" {
|
||||
break
|
||||
}
|
||||
result.append(char)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Get the descriptors of a display from the Edid
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - edid: the EDID of a display
|
||||
/// - type: the type of descriptor
|
||||
/// - Returns: a string if type of descriptor is found
|
||||
static func getDescriptorString(_ edid: EDID, _ type: UInt8) -> String? {
|
||||
for (_, descriptor) in Mirror(reflecting: edid.descriptors).children {
|
||||
if let descriptor = descriptor as? descriptor {
|
||||
if descriptor.text.type == UInt8(type) {
|
||||
return getEdidString(descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Get the name of a display
|
||||
///
|
||||
/// - Parameter edid: the EDID of a display
|
||||
/// - Returns: a string
|
||||
static func getDisplayName(forEdid edid: EDID) -> String {
|
||||
return getDescriptorString(edid, 0xFC) ?? NSLocalizedString("Display", comment: "")
|
||||
}
|
||||
|
||||
/// Get the serial of a display
|
||||
///
|
||||
/// - Parameter edid: the EDID of a display
|
||||
/// - Returns: a string
|
||||
static func getDisplaySerial(forEdid edid: EDID) -> String {
|
||||
return getDescriptorString(edid, 0xFF) ?? NSLocalizedString("Unknown", comment: "")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
10
Podfile
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Podfile
|
||||
|
||||
plateform :osx, '10.11'
|
||||
|
||||
target 'MonitorControl' do
|
||||
use_frameworks!
|
||||
|
||||
pod 'MediaKeyTap', :git => 'https://github.com/the0neyouseek/MediaKeyTap.git'
|
||||
|
||||
end
|
||||
12
Podfile.lock
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
PODS:
|
||||
- MediaKeyTap (1.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- MediaKeyTap
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
MediaKeyTap: 92764246d2ce8bf4446c2457e8b180e0b88926a1
|
||||
|
||||
PODFILE CHECKSUM: 11c0e07cdb4651a81ff3269f5d50664df18716d4
|
||||
|
||||
COCOAPODS: 1.3.1
|
||||
52
README.md
|
|
@ -1,42 +1,47 @@
|
|||
# MonitorControl
|
||||
|
||||
Control your external monitor brightness, contrast or volume directly from a menulet or with keyboard shortcuts :
|
||||
|
||||
- Brightness: `⇧` + `⌃` + `⌥` + `⌘` + `↑/↓` (Shift + Control + Alt + Command + Up/Down arrows)
|
||||
- Volume: `⇧` + `⌃` + `⌥` + `⌘` + `←/→` (Shift + Control + Alt + Command + Left/Right arrows)
|
||||
- Mute: `⇧` + `⌃` + `⌥` + `⌘` + `-` (Shift + Control + Alt + Command + Minus)
|
||||
|
||||
(Ps. The keyboard shortcut only work for the default screen)
|
||||
Control your external monitor brightness, contrast or volume directly from a menulet or with keyboard native keys
|
||||
|
||||

|
||||
|
||||
*Bonus: Using keyboard keys display the native osd :*
|
||||
|
||||

|
||||
|
||||
|
||||
## Download
|
||||
|
||||
Go to [Release](https://github.com/the0neyouseek/MonitorControl/releases/latest) and download the latest `.dmg`
|
||||
|
||||
## Brightness/Volume default key
|
||||
You can use [Karabiner Elements](https://github.com/tekezo/Karabiner-Elements/) to use the default mac key (`F1`, `F2` for brightness and `F10`, `F11`, `F12` for volume) with this set of custom rules :
|
||||
[Karabiner rules for MonitorControl](./.github/rules.json)
|
||||
## How to help
|
||||
|
||||
Copy and paste this url in your browser to install them directly :
|
||||
Open [issues](./issues) if you have a question, an enhancement to suggest or a bug you've found. If you want you can fork the code yourself and submit a pull request to improve the app.
|
||||
|
||||
```
|
||||
karabiner://karabiner/assets/complex_modifications/import?url=https%3A%2F%2Fraw.githubusercontent.com%2Fthe0neyouseek%2FMonitorControl%2Fmaster%2F.github%2Frules.json
|
||||
## How to build
|
||||
|
||||
### Required
|
||||
|
||||
- XCode
|
||||
- [Cocoapods](https://cocoapods.org/)
|
||||
- [SwiftLint](https://github.com/realm/SwiftLint)
|
||||
|
||||
Download the [zip](https://github.com/the0neyouseek/MonitorControl/archive/master.zip) directly or clone the project somewhere with git
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/the0neyouseek/MonitorControl.git
|
||||
```
|
||||
|
||||
---
|
||||
Then download the dependencies with Cocoapods
|
||||
|
||||
Bonus: Using keyboard shortcuts display the native osd :
|
||||
```sh
|
||||
$ pod install
|
||||
```
|
||||
|
||||

|
||||
You're all set ! Now open the `MonitorControl.xcworkspace` with Xcode
|
||||
|
||||
## TODO
|
||||
### Third party dependencies
|
||||
|
||||
- [ ] Hande multiple screen for keyboard shortcut (Possibly the choice to have all screen brightness/volume increase/decrease at the same time or separatly)
|
||||
- [ ] Skip Karabiner use for keyboard shortcut
|
||||
- [ ] Option to start app at login
|
||||
- [ ] Add [SwiftLint](https://github.com/realm/SwiftLint)
|
||||
- [ ] Change App Icon
|
||||
- [MediaKeyTap](https://github.com/the0neyouseek/MediaKeyTap)
|
||||
|
||||
## Support
|
||||
- macOS Sierra (`10.12`) and up.
|
||||
|
|
@ -45,4 +50,5 @@ Bonus: Using keyboard shortcuts display the native osd :
|
|||
## Thanks
|
||||
- [@bluejamesbond](https://github.com/bluejamesbond/) (Original developer)
|
||||
- [@Tyilo](https://github.com/Tyilo/) (Fork)
|
||||
- [@Bensge](https://github.com/Bensge/) - (Used some code from his project [NativeDisplayBrightness](https://github.com/Bensge/NativeDisplayBrightness))
|
||||
- [@Bensge](https://github.com/Bensge/) - (Used some code from his project [NativeDisplayBrightness](https://github.com/Bensge/NativeDisplayBrightness))
|
||||
- [@nhurden](https://github.com/nhurden/) (For the original MediaKeyTap)
|
||||