├── .gitignore ├── ApplyColorProfile.swift ├── Headers ├── Bridging-Header.h ├── MonitorPanel.framework │ └── Headers │ │ ├── CDStructures.h │ │ ├── MPDisplay.h │ │ ├── MPDisplayMgr.h │ │ ├── MPDisplayMode.h │ │ ├── MPDisplayModeBucket.h │ │ ├── MPDisplayPreset.h │ │ ├── MonitorPanel.h │ │ └── NSUUID-Compare.h └── OSD.framework │ └── Headers │ ├── OSDManager.h │ └── OSDUIHelperProtocol.h ├── HorizontalMonitorLayout.swift ├── IsCameraOn.swift ├── IsNowPlaying.swift ├── LICENSE ├── Makefile ├── MirrorMacBookToMonitor.swift ├── README.md ├── ReferencePreset.swift ├── RotateDisplay.swift ├── SendMediaKey.swift ├── SetKeyboardBacklight.swift ├── SetNativeBrightness.swift ├── SwapMonitors.swift ├── ToggleHDR.swift ├── VerticalMonitorLayout.swift ├── bin ├── ApplyColorProfile ├── HorizontalMonitorLayout ├── IsCameraOn ├── IsNowPlaying ├── MirrorMacBookToMonitor ├── ReferencePreset ├── RotateDisplay ├── SendMediaKey ├── SetKeyboardBacklight ├── SetNativeBrightness ├── SwapMonitors ├── ToggleHDR ├── VerticalMonitorLayout ├── com.apple.controlcenter.mac-utils.IsNowPlaying ├── runbg └── runfg ├── img └── add-to-shortcuts.svg ├── lib └── Extensions.swift ├── mac-utils.sublime-project ├── runbg.swift └── runfg.swift /.gitignore: -------------------------------------------------------------------------------- 1 | bin/*-x86 2 | bin/*-arm64 3 | Screens.swift 4 | bin/Screens 5 | -------------------------------------------------------------------------------- /ApplyColorProfile.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import ColorSync 3 | import CoreGraphics 4 | import Foundation 5 | 6 | let FACTORY_PROFILES = kColorSyncFactoryProfiles.takeUnretainedValue() as String 7 | let DEVICE_PROFILE_URL = kColorSyncDeviceProfileURL.takeUnretainedValue() as String 8 | let DEVICE_CLASS = kColorSyncDisplayDeviceClass.takeUnretainedValue() 9 | let DEFAULT_PROFILE = kColorSyncDeviceDefaultProfileID.takeUnretainedValue() 10 | 11 | extension UUID { 12 | var cfUUID: CFUUID { 13 | let b = uuid 14 | return CFUUIDCreateWithBytes(nil, b.0, b.1, b.2, b.3, b.4, b.5, b.6, b.7, b.8, b.9, b.10, b.11, b.12, b.13, b.14, b.15) 15 | } 16 | } 17 | 18 | func setDisplayProfile(_ path: String, _ display: MPDisplay) { 19 | if path == "factory" { 20 | print("Resetting profile for \(display.displayName ?? "display") to factory default") 21 | 22 | let profileDict = [DEFAULT_PROFILE: nil] as CFDictionary 23 | guard ColorSyncDeviceSetCustomProfiles(DEVICE_CLASS, display.uuid.cfUUID, profileDict) else { 24 | print("Failed to set factory profile") 25 | return 26 | } 27 | return 28 | } 29 | 30 | print("Setting profile \(path) for \(display.displayName ?? "display")") 31 | let iccURL = URL(fileURLWithPath: path) 32 | let uuid = display.uuid.cfUUID 33 | 34 | var err: Unmanaged? 35 | guard let profile = ColorSyncProfileCreateWithURL(iccURL as CFURL, &err)?.takeRetainedValue() else { 36 | print("Failed to create profile from \(path)") 37 | return 38 | } 39 | 40 | let profileName = ColorSyncProfileCopyDescriptionString(profile)?.takeRetainedValue() as String? ?? iccURL.deletingPathExtension().lastPathComponent 41 | print("Profile name: \(profileName)") 42 | 43 | guard let deviceInfo = ColorSyncDeviceCopyDeviceInfo(DEVICE_CLASS, uuid)?.takeRetainedValue() else { 44 | print("Failed to get device info for \(uuid)") 45 | return 46 | } 47 | print("Device info: \(deviceInfo)") 48 | 49 | print("Setting profile \(profileName) for \(uuid)") 50 | let profileDict = [DEFAULT_PROFILE: iccURL] as CFDictionary 51 | guard ColorSyncDeviceSetCustomProfiles(DEVICE_CLASS, uuid, profileDict) else { 52 | print("Failed to set custom profile") 53 | return 54 | } 55 | } 56 | 57 | func main() { 58 | guard let mgr = MPDisplayMgr(), let displays = mgr.displays else { 59 | print("No displays") 60 | return 61 | } 62 | 63 | guard CommandLine.arguments.count >= 3 else { 64 | print(""" 65 | Usage: \(CommandLine.arguments[0]) 66 | 67 | display: Can be a display ID, UUID, or name. Use "all" to apply to all displays. 68 | profile: Path to an ICC profile. Use "factory" to reset to default. 69 | """) 70 | return 71 | } 72 | 73 | let display = CommandLine.arguments[1] 74 | let profilePath = CommandLine.arguments[2] 75 | guard FileManager.default.fileExists(atPath: profilePath) || profilePath == "factory" else { 76 | print("File not found: \(profilePath)") 77 | return 78 | } 79 | 80 | // Example: `ApplyColorProfile all HighAmbientLight.icc` 81 | if display.lowercased() == "all" { 82 | for display in displays.filter(\.hasPresets) { 83 | setDisplayProfile(profilePath, display) 84 | } 85 | return 86 | } 87 | 88 | // Example: `ApplyColorProfile DELL HighAmbientLight.icc` 89 | guard let display = mgr.matchDisplay(filter: display) else { 90 | print("No display found for query: \(display)") 91 | 92 | return 93 | } 94 | 95 | setDisplayProfile(profilePath, display) 96 | } 97 | 98 | main() 99 | -------------------------------------------------------------------------------- /Headers/Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | 11 | @class BrightnessSystemClient; 12 | 13 | @interface KeyboardBrightnessClient : NSObject 14 | { 15 | BrightnessSystemClient *bsc; 16 | } 17 | 18 | - (void)registerNotificationForKeys:(id _Nonnull)arg1 keyboardID:(unsigned long long)arg2 block:(void)arg3; 19 | - (void)unregisterKeyboardNotificationBlock; 20 | - (BOOL)isAutoBrightnessEnabledForKeyboard:(unsigned long long)arg1; 21 | - (BOOL)setIdleDimTime:(double)arg1 forKeyboard:(unsigned long long)arg2; 22 | - (double)idleDimTimeForKeyboard:(unsigned long long)arg1; 23 | - (BOOL)isKeyboardBuiltIn:(unsigned long long)arg1; 24 | - (BOOL)isAmbientFeatureAvailableOnKeyboard:(unsigned long long)arg1; 25 | - (BOOL)enableAutoBrightness:(BOOL)arg1 forKeyboard:(unsigned long long)arg2; 26 | - (BOOL)setBrightness:(float)arg1 forKeyboard:(unsigned long long)arg2; 27 | - (float)brightnessForKeyboard:(unsigned long long)arg1; 28 | - (BOOL)isBacklightDimmedOnKeyboard:(unsigned long long)arg1; 29 | - (BOOL)isBacklightSaturatedOnKeyboard:(unsigned long long)arg1; 30 | - (BOOL)isBacklightSuppressedOnKeyboard:(unsigned long long)arg1; 31 | - (NSArray *_Nullable)copyKeyboardBacklightIDs; 32 | - (void)dealloc; 33 | - (id _Nonnull)init; 34 | 35 | @end 36 | 37 | double CoreDisplay_Display_GetUserBrightness(CGDirectDisplayID display); 38 | double CoreDisplay_Display_GetLinearBrightness(CGDirectDisplayID display); 39 | double CoreDisplay_Display_GetDynamicLinearBrightness(CGDirectDisplayID display); 40 | 41 | void CoreDisplay_Display_SetUserBrightness(CGDirectDisplayID display, double brightness); 42 | void CoreDisplay_Display_SetLinearBrightness(CGDirectDisplayID display, double brightness); 43 | void CoreDisplay_Display_SetDynamicLinearBrightness(CGDirectDisplayID display, double brightness); 44 | 45 | void CoreDisplay_Display_SetAutoBrightnessIsEnabled(CGDirectDisplayID, bool); 46 | 47 | CFDictionaryRef CoreDisplay_DisplayCreateInfoDictionary(CGDirectDisplayID); 48 | 49 | int DisplayServicesGetLinearBrightness(CGDirectDisplayID display, float *brightness); 50 | int DisplayServicesSetLinearBrightness(CGDirectDisplayID display, float brightness); 51 | int DisplayServicesGetBrightness(CGDirectDisplayID display, float *brightness); 52 | int DisplayServicesSetBrightness(CGDirectDisplayID display, float brightness); 53 | int DisplayServicesSetBrightnessSmooth(CGDirectDisplayID display, float brightness); 54 | bool DisplayServicesCanChangeBrightness(CGDirectDisplayID display); 55 | bool DisplayServicesHasAmbientLightCompensation(CGDirectDisplayID display); 56 | bool DisplayServicesAmbientLightCompensationEnabled(CGDirectDisplayID display); 57 | bool DisplayServicesIsSmartDisplay(CGDirectDisplayID display); 58 | 59 | extern int SLSMainConnectionID(void); 60 | CGError SLSGetDisplayList(uint32_t maxDisplays, CGDirectDisplayID *activeDisplays, uint32_t *displayCount); 61 | CGError SLSGetZoomParameters(int cid, CGPoint *origin, double *zoomFactor, bool *smoothed); 62 | 63 | bool IsLidClosed(void) 64 | { 65 | bool isClosed = false; 66 | io_registry_entry_t rootDomain; 67 | mach_port_t masterPort; 68 | CFTypeRef clamShellStateRef = NULL; 69 | 70 | IOReturn ioReturn = IOMasterPort(MACH_PORT_NULL, &masterPort); 71 | if (ioReturn != 0) 72 | { 73 | return false; 74 | } 75 | 76 | rootDomain = IORegistryEntryFromPath(masterPort, kIOPowerPlane ":/IOPowerConnection/IOPMrootDomain"); 77 | 78 | clamShellStateRef = IORegistryEntryCreateCFProperty(rootDomain, CFSTR("AppleClamshellState"), kCFAllocatorDefault, 0); 79 | if (clamShellStateRef == NULL) 80 | { 81 | if (rootDomain) 82 | { 83 | IOObjectRelease(rootDomain); 84 | return false; 85 | } 86 | } 87 | 88 | if (CFBooleanGetValue((CFBooleanRef)(clamShellStateRef)) == true) 89 | { 90 | isClosed = true; 91 | } 92 | 93 | if (rootDomain) 94 | { 95 | IOObjectRelease(rootDomain); 96 | } 97 | 98 | if (clamShellStateRef) 99 | { 100 | CFRelease(clamShellStateRef); 101 | } 102 | 103 | return isClosed; 104 | } 105 | -------------------------------------------------------------------------------- /Headers/MonitorPanel.framework/Headers/CDStructures.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by class-dump 3.5 (64 bit). 3 | // 4 | // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. 5 | // 6 | 7 | #pragma mark Named Structures 8 | 9 | struct _CGSDisplayModeDescription { 10 | int displayModeNumber; 11 | int flags; 12 | int width; 13 | int height; 14 | int depth; 15 | int rowBytes; 16 | int bitsPerPixel; 17 | int bitsPerSample; 18 | int samplesPerPixel; 19 | int refreshRate; 20 | int horizontalResolution; 21 | int verticalResolution; 22 | char encoding[129]; 23 | int version; 24 | int length; 25 | int fixPtRefreshRate; 26 | int ioModeInfoFlags; 27 | int ioDisplayModeNumber; 28 | int pixelsWide; 29 | int pixelsHigh; 30 | float resolution; 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /Headers/MonitorPanel.framework/Headers/MPDisplay.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by class-dump 3.5 (64 bit). 3 | // 4 | // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. 5 | // 6 | 7 | #import 8 | #include 9 | 10 | @class MPDisplayMode, MPDisplayPreset, NSArray, NSImage, NSMutableArray, NSMutableDictionary, NSString, NSUUID; 11 | 12 | @interface MPDisplay : NSObject 13 | { 14 | int _displayID; 15 | int _aliasID; 16 | unsigned int _ioService; 17 | NSString *_displayName; 18 | unsigned int _userFlags; 19 | NSMutableArray *_allModes; 20 | NSMutableArray *_trimmedModes; 21 | NSMutableArray *_listHQModes; 22 | NSMutableArray *_listModes; 23 | NSMutableArray *_userModes; 24 | NSMutableDictionary *_trimmedResolutions; 25 | NSMutableDictionary *_listHQResolutions; 26 | NSMutableDictionary *_listResolutions; 27 | NSMutableDictionary *_userResolutions; 28 | NSMutableArray *_scanRates; 29 | NSMutableArray *_scanRateStrings; 30 | MPDisplayMode *_defaultMode; 31 | MPDisplayMode *_nativeMode; 32 | MPDisplayMode *_currentMode; 33 | int _orientation; 34 | NSUUID *_uuid; 35 | NSImage *_displayIcon; 36 | NSImage *_displayResolutionPreviewIcon; 37 | struct CGRect _displayResolutionPreviewRect; 38 | BOOL _isSmartDisplay; 39 | BOOL _hasSafeMode; 40 | BOOL _hasSimulscan; 41 | BOOL _hasTVModes; 42 | BOOL _isHiDPI; 43 | BOOL _isRetina; 44 | BOOL _isBuiltIn; 45 | BOOL _hqModesForChiclets; 46 | BOOL _isMirrored; 47 | BOOL _isMirrorMaster; 48 | BOOL _isTV; 49 | BOOL _isProjector; 50 | BOOL _isAirPlayDisplay; 51 | BOOL _is4K; 52 | BOOL _isSidecarDisplay; 53 | BOOL _hasMultipleRates; 54 | BOOL _hasZeroRate; 55 | BOOL _uiProjectorOverride; 56 | BOOL _needsUpdate; 57 | struct os_unfair_lock_s _accessLock; 58 | NSMutableArray *_presetsArray; 59 | unsigned int _numberOfPresets; 60 | BOOL _hasRotationSensor; 61 | BOOL _usePreciseRefreshRate; 62 | BOOL _loadingPresets; 63 | BOOL _isAppleProDisplay; 64 | } 65 | 66 | @property(readonly) BOOL hasRotationSensor; // @synthesize hasRotationSensor=_hasRotationSensor; 67 | @property(retain) MPDisplayMode *defaultMode; // @synthesize defaultMode=_defaultMode; 68 | @property(retain) MPDisplayMode *nativeMode; // @synthesize nativeMode=_nativeMode; 69 | @property(retain) MPDisplayMode *currentMode; // @synthesize currentMode=_currentMode; 70 | @property(readonly) BOOL hasZeroRate; // @synthesize hasZeroRate=_hasZeroRate; 71 | @property(readonly) BOOL hasMultipleRates; // @synthesize hasMultipleRates=_hasMultipleRates; 72 | @property(readonly) BOOL isSidecarDisplay; // @synthesize isSidecarDisplay=_isSidecarDisplay; 73 | @property(readonly) BOOL isAirPlayDisplay; // @synthesize isAirPlayDisplay=_isAirPlayDisplay; 74 | @property(readonly) BOOL isProjector; // @synthesize isProjector=_isProjector; 75 | @property(readonly) BOOL is4K; // @synthesize is4K=_is4K; 76 | @property(readonly) BOOL isTV; // @synthesize isTV=_isTV; 77 | @property(readonly) BOOL isMirrorMaster; // @synthesize isMirrorMaster=_isMirrorMaster; 78 | @property(readonly) BOOL isMirrored; // @synthesize isMirrored=_isMirrored; 79 | @property(readonly) BOOL isBuiltIn; // @synthesize isBuiltIn=_isBuiltIn; 80 | @property(readonly) BOOL isHiDPI; // @synthesize isHiDPI=_isHiDPI; 81 | @property(readonly) BOOL hasTVModes; // @synthesize hasTVModes=_hasTVModes; 82 | @property(readonly) BOOL hasSimulscan; // @synthesize hasSimulscan=_hasSimulscan; 83 | @property(readonly) BOOL hasSafeMode; // @synthesize hasSafeMode=_hasSafeMode; 84 | @property(readonly) BOOL isSmartDisplay; // @synthesize isSmartDisplay=_isSmartDisplay; 85 | @property(nonatomic) int orientation; // @synthesize orientation=_orientation; 86 | @property unsigned int userFlags; // @synthesize userFlags=_userFlags; 87 | @property(readonly) int aliasID; // @synthesize aliasID=_aliasID; 88 | @property(readonly) int displayID; // @synthesize displayID=_displayID; 89 | - (BOOL)setActivePreset:(MPDisplayPreset *)arg1; 90 | @property(readonly) NSArray *presets; 91 | @property(readonly) BOOL hasPresets; 92 | @property(readonly) MPDisplayPreset *defaultPreset; 93 | - (void)buildPresetsList; 94 | @property(readonly) MPDisplayPreset *activePreset; 95 | - (void)_loadPreviewIconFromServiceDictionary:(id)arg1; 96 | - (id)_imageAndRect:(struct CGRect *)arg1 fromDictionary:(id)arg2 forOrientation:(long long)arg3; 97 | - (id)_iconAtPath:(id)arg1; 98 | @property(readonly) struct CGRect displayResolutionPreviewRect; 99 | @property(readonly) NSImage *displayResolutionPreviewIcon; 100 | @property(readonly) NSImage *displayIcon; 101 | @property(readonly) NSUUID *uuid; 102 | @property(readonly) struct CGRect hardwareBounds; 103 | @property(readonly) int mirrorMasterDisplayID; 104 | @property(readonly) BOOL isForcedToMirror; 105 | - (void)setMirrorMaster:(BOOL)arg1; 106 | - (void)setMirrorMode:(id)arg1; 107 | - (void)setMirrorModeNumber:(int)arg1; 108 | - (int)setMode:(id)arg1; 109 | - (int)setModeNumber:(int)arg1; 110 | - (BOOL)isModeNative:(id)arg1; 111 | - (BOOL)inDefaultMode; 112 | - (id)modesForResolution:(id)arg1; 113 | @property(readonly) NSArray *scanRateStrings; 114 | @property(readonly) NSArray *scanRates; 115 | - (id)allModes; 116 | @property BOOL bestForVideoMode; 117 | - (BOOL)supportsBestForVideoMode; 118 | @property int underscan; 119 | @property(readonly) int maxUnderscan; 120 | @property(readonly) int minUnderscan; 121 | - (BOOL)supportsUnderscan; 122 | @property BOOL overscanEnabled; 123 | - (BOOL)supportsOverscan; 124 | - (BOOL)canChangeOrientation; 125 | - (BOOL)hasMultipleScanRates; 126 | - (BOOL)preferHDRModes; 127 | @property(readonly) struct CGRect displayBounds; 128 | - (MPDisplayMode *)modeWithNumber:(int)arg1; 129 | - (MPDisplayMode *)modeMatchingResolutionOfMode:(MPDisplayMode *)arg1 withScanRate:(NSNumber *)arg2; 130 | - (NSArray *)modesMatchingResolutionOfMode:(MPDisplayMode *)arg1; 131 | - (NSArray *)modesOfType:(unsigned long long)arg1; 132 | - (id)resolutionsOfType:(unsigned long long)arg1; 133 | - (void)refreshModes; 134 | - (void)refreshResolutions; 135 | - (void)refreshResolutions:(id)arg1 usingModeList:(id)arg2; 136 | - (NSNumber *)scanRateForString:(NSString *)arg1; 137 | - (NSString *)stringForScanRate:(NSNumber *)arg1; 138 | - (void)refreshScanRates; 139 | - (void)determineTrimmedModeList; 140 | - (void)bucketizeDisplayModes; 141 | - (void)addMatchingModesToTrimmed; 142 | - (void)addTVModesToPreferred; 143 | - (void)setPreferHDRModes:(BOOL)arg1; 144 | - (BOOL)isAlias:(int)arg1; 145 | - (id)multiscanModesForMode:(id)arg1; 146 | @property(readonly) BOOL hasMenuBar; 147 | @property(readonly) BOOL isAppleProDisplay; 148 | @property(readonly) BOOL isBuiltInRetina; 149 | @property(readonly) BOOL hasHDRModes; 150 | @property(readonly) NSString *titleName; 151 | @property(retain, nonatomic) NSString *displayName; 152 | - (void)dealloc; 153 | - (id)initWithCGSDisplayID:(int)arg1; 154 | 155 | 156 | @end 157 | -------------------------------------------------------------------------------- /Headers/MonitorPanel.framework/Headers/MPDisplayMgr.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by class-dump 3.5 (64 bit). 3 | // 4 | // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. 5 | // 6 | 7 | @class MPDisplay, NSArray, NSMutableArray; 8 | 9 | @interface MPDisplayMgr : NSObject 10 | { 11 | NSMutableArray *_displays; 12 | MPDisplay *_mainDisplay; 13 | BOOL _hwChanged; 14 | struct os_unfair_lock_s _hwChangeLock; 15 | struct os_unfair_lock_s _dataLock; 16 | struct os_unfair_lock_s _accessLock; 17 | BOOL _hasBuiltinRetina; 18 | BOOL _hasSmallBuiltinRetina; 19 | BOOL _safeMode; 20 | } 21 | 22 | + (id)sharedMgr; 23 | @property(readonly) BOOL runningInSafeMode; // @synthesize runningInSafeMode=_safeMode; 24 | @property(readonly) MPDisplay *mainDisplay; // @synthesize mainDisplay=_mainDisplay; 25 | @property(readonly) BOOL hasSmallBuiltinRetina; // @synthesize hasSmallBuiltinRetina=_hasSmallBuiltinRetina; 26 | @property(readonly) BOOL hasBuiltinRetina; // @synthesize hasBuiltinRetina=_hasBuiltinRetina; 27 | @property(readonly) BOOL hasNightShiftCabableDisplays; 28 | - (BOOL)tryLockAccess; 29 | - (void)unlockAccess; 30 | - (void)lockAccess; 31 | - (void)stopAllMirroring; 32 | - (void)createMirrorSet:(id)arg1; 33 | - (void)stopMirroringForDisplay:(id)arg1; 34 | - (id)mirrorMasterForDisplay:(id)arg1; 35 | - (id)mirrorSetForDisplay:(id)arg1; 36 | - (BOOL)isAnyDisplayMirrored; 37 | - (void)setMirrorState:(BOOL)arg1 useBestMode:(BOOL)arg2; 38 | - (void)mirrorAllDisplaysTo:(id)arg1 useBestMode:(BOOL)arg2; 39 | - (void)setMirrorMaster:(id)arg1 useBestMode:(BOOL)arg2; 40 | - (void)setMirrorMasterMode:(id)arg1; 41 | - (void)notifyWillReconfigure; 42 | - (void)notifyHardwareChange; 43 | - (void)notifyReconfigure; 44 | - (void)postNotification:(id)arg1; 45 | - (void)serviceReconfigure:(id)arg1; 46 | - (void)refreshDisplays; 47 | - (id)displayWithID:(int)arg1; 48 | @property(readonly) NSArray *displays; 49 | - (void)updateDisplaysList; 50 | - (void)nameDisplays:(id)arg1; 51 | - (void)dealloc; 52 | - (void)removeNotifications; 53 | - (id)init; 54 | 55 | @end 56 | 57 | -------------------------------------------------------------------------------- /Headers/MonitorPanel.framework/Headers/MPDisplayMode.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by class-dump 3.5 (64 bit). 3 | // 4 | // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. 5 | // 6 | 7 | #import "CDStructures.h" 8 | 9 | @class MPDisplay, NSNumber, NSString; 10 | 11 | @interface MPDisplayMode : NSObject 12 | { 13 | MPDisplay *_display; 14 | struct _CGSDisplayModeDescription _desc; 15 | NSString *_rateString; 16 | unsigned int _tvMode; 17 | } 18 | 19 | + (id)modeWithDescription:(struct _CGSDisplayModeDescription *)arg1 forDisplay:(id)arg2; 20 | @property(readonly) MPDisplay *display; // @synthesize display=_display; 21 | @property(readonly) NSString *refreshString; 22 | @property(readonly) NSString *resolutionString; 23 | - (id)resolutionFormat; 24 | @property(readonly) BOOL isSafeMode; 25 | - (id)description; 26 | - (void)getModeDescription:(struct _CGSDisplayModeDescription *)arg1; 27 | - (BOOL)modeResolutionMatches:(id)arg1; 28 | - (BOOL)resolutionMatches:(struct _CGSDisplayModeDescription *)arg1; 29 | @property(readonly) unsigned int tvModeEquiv; 30 | @property(readonly) unsigned int tvMode; 31 | @property(readonly) BOOL isTVMode; 32 | @property(readonly) BOOL isSimulscan; 33 | @property(readonly) BOOL isInterlaced; 34 | @property(readonly) BOOL isNativeMode; 35 | @property(readonly) BOOL isDefaultMode; 36 | @property(readonly) BOOL isStretched; 37 | @property(readonly) BOOL isUserVisible; 38 | @property(readonly) BOOL isHiDPI; 39 | @property(readonly) BOOL isRetina; 40 | @property(readonly) NSNumber *scanRate; 41 | @property(readonly) int roundedScanRate; 42 | @property(readonly) float scale; 43 | @property(readonly) float aspectRatio; 44 | @property(readonly) int fixPtRefreshRate; 45 | @property(readonly) int refreshRate; 46 | @property(readonly) int dotsPerInch; 47 | @property(readonly) int vertDPI; 48 | @property(readonly) int horizDPI; 49 | @property(readonly) int pixelsHigh; 50 | @property(readonly) int pixelsWide; 51 | @property(readonly) int height; 52 | @property(readonly) int width; 53 | @property(readonly) int modeNumber; 54 | @property(readonly) struct _CGSDisplayModeDescription *modeDescription; 55 | - (void)dealloc; 56 | - (id)initWithModeDescription:(struct _CGSDisplayModeDescription *)arg1 forDisplay:(id)arg2; 57 | 58 | @end 59 | 60 | -------------------------------------------------------------------------------- /Headers/MonitorPanel.framework/Headers/MPDisplayModeBucket.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by class-dump 3.5 (64 bit). 3 | // 4 | // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. 5 | // 6 | 7 | #import "NSObject.h" 8 | 9 | @class NSMutableArray; 10 | 11 | @interface MPDisplayModeBucket : NSObject 12 | { 13 | float _minHeight; 14 | float _maxHeight; 15 | float _targetHeight; 16 | BOOL _usePreciseRate; 17 | NSMutableArray *_modes; 18 | } 19 | 20 | - (id)bestModeForNativeHeight:(float)arg1; 21 | - (id)bestMode; 22 | - (id)lowMode; 23 | - (id)highMode; 24 | - (BOOL)addModeToBucket:(id)arg1; 25 | - (BOOL)modeFitsBucket:(id)arg1; 26 | - (void)dealloc; 27 | - (id)initWithMinHeight:(float)arg1 maxHeight:(float)arg2 targetHeight:(float)arg3 usingPreciseRate:(BOOL)arg4; 28 | 29 | @end 30 | 31 | -------------------------------------------------------------------------------- /Headers/MonitorPanel.framework/Headers/MPDisplayPreset.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by class-dump 3.5 (64 bit). 3 | // 4 | // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. 5 | // 6 | 7 | #import 8 | 9 | @class NSDictionary, NSString; 10 | 11 | @interface MPDisplayPreset : NSObject 12 | { 13 | unsigned int _displayID; 14 | NSString *_presetName; 15 | NSDictionary *_presetDictionary; 16 | unsigned long long _presetIndex; 17 | BOOL _isWritable; 18 | long long _presetGroup; 19 | } 20 | 21 | @property(readonly) long long presetGroup; // @synthesize presetGroup=_presetGroup; 22 | @property(readonly) BOOL isWritable; // @synthesize isWritable=_isWritable; 23 | @property(readonly) unsigned long long presetIndex; // @synthesize presetIndex=_presetIndex; 24 | @property(readonly) NSDictionary *presetDictionary; // @synthesize presetDictionary=_presetDictionary; 25 | @property(readonly) unsigned int displayID; // @synthesize displayID=_displayID; 26 | - (void)_safeCopyKey:(id)arg1 fromDict:(id)arg2 toDict:(id)arg3; 27 | - (id)_mutableDictionaryWithNewSettings:(id)arg1; 28 | - (void)_initializeWithIndex:(unsigned int)arg1 dictionary:(struct __CFDictionary *)arg2 forDisplayID:(unsigned int)arg3; 29 | - (id)_getStringFromStringRef:(struct __CFString *)arg1; 30 | @property(readonly) NSString *presetName; 31 | @property(readonly) NSString *presetDescription; 32 | - (BOOL)makeValidWithSettings:(id)arg1; 33 | - (BOOL)makeInvalid; 34 | @property(readonly) BOOL isValid; 35 | - (void)dealloc; 36 | - (id)initWithIndex:(unsigned int)arg1 dictionary:(struct __CFDictionary *)arg2 forDisplayID:(unsigned int)arg3; 37 | 38 | @end 39 | 40 | -------------------------------------------------------------------------------- /Headers/MonitorPanel.framework/Headers/MonitorPanel.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by class-dump 3.5 (64 bit). 3 | // 4 | // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. 5 | // 6 | 7 | #import "NSObject.h" 8 | 9 | @class NSBundle, NSView; 10 | 11 | @interface MonitorPanel : NSObject 12 | { 13 | int _displayID; 14 | NSBundle *_bundle; 15 | NSView *_view; 16 | id _controller; 17 | } 18 | 19 | + (int)fidelityForDisplayID:(int)arg1; 20 | - (void)setController:(id)arg1; 21 | - (id)controller; 22 | - (void)setBundle:(id)arg1; 23 | - (id)bundle; 24 | - (void)didChangeScreenParameters; 25 | - (void)displayHardwareChanged; 26 | - (void)displayDidReconfigure; 27 | - (void)displayWillReconfigure; 28 | - (void)didSelect; 29 | - (void)willSelect; 30 | - (void)didUnselect; 31 | - (void)willUnselect; 32 | - (BOOL)shouldUnselect; 33 | - (id)nibName; 34 | - (unsigned int)type; 35 | - (unsigned int)family; 36 | - (int)minorOrder; 37 | - (int)majorOrder; 38 | - (id)view; 39 | - (void)didLoadNib; 40 | - (void)dealloc; 41 | - (id)initWithDisplayID:(int)arg1; 42 | 43 | @end 44 | 45 | -------------------------------------------------------------------------------- /Headers/MonitorPanel.framework/Headers/NSUUID-Compare.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by class-dump 3.5 (64 bit). 3 | // 4 | // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. 5 | // 6 | 7 | #import "NSUUID.h" 8 | 9 | @interface NSUUID (Compare) 10 | - (long long)compare:(id)arg1; 11 | @end 12 | 13 | -------------------------------------------------------------------------------- /Headers/OSD.framework/Headers/OSDManager.h: -------------------------------------------------------------------------------- 1 | #import "OSDUIHelperProtocol.h" 2 | 3 | @class NSXPCConnection; 4 | 5 | @interface OSDManager : NSObject 6 | { 7 | id _proxyObject; 8 | NSXPCConnection *connection; 9 | } 10 | 11 | + (id)sharedManager; 12 | @property(retain) NSXPCConnection *connection; // @synthesize connection; 13 | - (void)showFullScreenImage:(long long)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecToAnimate:(unsigned int)arg4; 14 | - (void)fadeClassicImageOnDisplay:(unsigned int)arg1; 15 | - (void)showImageAtPath:(id)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecUntilFade:(unsigned int)arg4 withText:(id)arg5; 16 | - (void)showImage:(long long)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecUntilFade:(unsigned int)arg4 filledChiclets:(unsigned int)arg5 totalChiclets:(unsigned int)arg6 locked:(BOOL)arg7; 17 | - (void)showImage:(long long)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecUntilFade:(unsigned int)arg4 withText:(id)arg5; 18 | - (void)showImage:(long long)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecUntilFade:(unsigned int)arg4; 19 | @property(readonly) id remoteObjectProxy; // @dynamic remoteObjectProxy; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Headers/OSD.framework/Headers/OSDUIHelperProtocol.h: -------------------------------------------------------------------------------- 1 | @class NSString; 2 | 3 | @protocol OSDUIHelperProtocol 4 | - (void)showFullScreenImage:(long long)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecToAnimate:(unsigned int)arg4; 5 | - (void)fadeClassicImageOnDisplay:(unsigned int)arg1; 6 | - (void)showImageAtPath:(NSString *)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecUntilFade:(unsigned int)arg4 withText:(NSString *)arg5; 7 | - (void)showImage:(long long)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecUntilFade:(unsigned int)arg4 filledChiclets:(unsigned int)arg5 totalChiclets:(unsigned int)arg6 locked:(BOOL)arg7; 8 | - (void)showImage:(long long)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecUntilFade:(unsigned int)arg4 withText:(NSString *)arg5; 9 | - (void)showImage:(long long)arg1 onDisplayID:(unsigned int)arg2 priority:(unsigned int)arg3 msecUntilFade:(unsigned int)arg4; 10 | @end 11 | 12 | -------------------------------------------------------------------------------- /HorizontalMonitorLayout.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | import Cocoa 3 | import Foundation 4 | 5 | configure { config in 6 | print("Usage: \(CommandLine.arguments[0]) [left|right] (default: right)\n") 7 | 8 | let macBookDisplay = CGMainDisplayID() 9 | guard let otherDisplay = NSScreen.onlineDisplayIDs.first(where: { $0 != macBookDisplay }) else { 10 | print("No external display detected") 11 | return false 12 | } 13 | 14 | let macBookBounds = CGDisplayBounds(macBookDisplay) 15 | let monitorBounds = CGDisplayBounds(otherDisplay) 16 | print( 17 | "Main Display: x=\(macBookBounds.origin.x) y=\(macBookBounds.origin.y) width=\(macBookBounds.width) height=\(macBookBounds.height)" 18 | ) 19 | print( 20 | "External Display: x=\(monitorBounds.origin.x) y=\(monitorBounds.origin.y) width=\(monitorBounds.width) height=\(monitorBounds.height)" 21 | ) 22 | 23 | let monitorX = CommandLine.arguments.contains("left") ? -monitorBounds.width : monitorBounds.width 24 | let monitorY = (max(monitorBounds.height, macBookBounds.height) - min(monitorBounds.height, macBookBounds.height)) / -2 25 | 26 | print("\nNew external display coordinates: x=\(monitorX) y=\(monitorY)") 27 | CGConfigureDisplayOrigin(config, otherDisplay, Int32(monitorX.rounded()), Int32(monitorY.rounded())) 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /IsCameraOn.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import CoreMediaIO 3 | 4 | // Borrowed from https://github.com/wouterdebie/onair/blob/master/Sources/onair/Camera.swift 5 | 6 | if CommandLine.arguments.count >= 2, ["-h", "--help"].contains(CommandLine.arguments[1]) { 7 | print("Usage: \(CommandLine.arguments[0]) [-q (exits with non-zero status code if camera is not on)]") 8 | exit(0) 9 | } 10 | 11 | // MARK: - Camera 12 | 13 | class Camera: CustomStringConvertible { 14 | init(captureDevice: AVCaptureDevice) { 15 | self.captureDevice = captureDevice 16 | id = captureDevice.value(forKey: "_connectionID")! as! CMIOObjectID 17 | 18 | // Figure out if this is a virtual (DAL) device or an actual hardware device. 19 | // It seems that DAL devices have kCMIODevicePropertyLatency, while normal 20 | // AVCaptureDevices don't. 21 | var latencyPA = CMIOObjectPropertyAddress( 22 | mSelector: CMIOObjectPropertySelector(kCMIODevicePropertyLatency), 23 | mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeWildcard), 24 | mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementWildcard) 25 | ) 26 | var dataSize = UInt32(0) 27 | 28 | if CMIOObjectGetPropertyDataSize(id, &latencyPA, 0, nil, &dataSize) == OSStatus(kCMIOHardwareNoError) { 29 | isVirtual = true 30 | } 31 | } 32 | 33 | public var description: String { "\(captureDevice.manufacturer)/\(captureDevice.localizedName)" } 34 | 35 | var isVirtual = false 36 | 37 | func isOn() -> Bool { 38 | // Test if the device is on through some magic. If the pointee is > 0, the device is active. 39 | var (dataSize, dataUsed) = (UInt32(0), UInt32(0)) 40 | if CMIOObjectGetPropertyDataSize(id, &STATUS_PA, 0, nil, &dataSize) == OSStatus(kCMIOHardwareNoError) { 41 | if let data = malloc(Int(dataSize)) { 42 | CMIOObjectGetPropertyData(id, &STATUS_PA, 0, nil, dataSize, &dataUsed, data) 43 | return data.assumingMemoryBound(to: UInt8.self).pointee > 0 44 | } 45 | } 46 | return false 47 | } 48 | 49 | private var id: CMIOObjectID 50 | private var captureDevice: AVCaptureDevice 51 | private var STATUS_PA = CMIOObjectPropertyAddress( 52 | mSelector: CMIOObjectPropertySelector(kCMIODevicePropertyDeviceIsRunningSomewhere), 53 | mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeWildcard), 54 | mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementWildcard) 55 | ) 56 | } 57 | 58 | let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .unspecified) 59 | let devices = discoverySession.devices 60 | var cameraInUse = false 61 | 62 | for device in devices { 63 | let camera = Camera(captureDevice: device) 64 | if camera.isOn() { 65 | cameraInUse = true 66 | break 67 | } 68 | } 69 | 70 | guard CommandLine.arguments.count >= 2, CommandLine.arguments[1] == "-q" else { 71 | print(cameraInUse) 72 | exit(0) 73 | } 74 | 75 | exit(cameraInUse ? 0 : 1) 76 | -------------------------------------------------------------------------------- /IsNowPlaying.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | import Foundation 3 | 4 | if CommandLine.arguments.count >= 2, ["-h", "--help"].contains(CommandLine.arguments[1]) { 5 | print("Usage: \(CommandLine.arguments[0]) [-q (exits with non-zero status code if not playing)] [-v (prints now playing info)]") 6 | exit(0) 7 | } 8 | 9 | let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework"))! 10 | 11 | let MRMediaRemoteGetNowPlayingApplicationIsPlayingPointer = CFBundleGetFunctionPointerForName( 12 | bundle, 13 | "MRMediaRemoteGetNowPlayingApplicationIsPlaying" as CFString 14 | )! 15 | typealias MRMediaRemoteGetNowPlayingApplicationIsPlayingFunction = @convention(c) (DispatchQueue, @escaping (Bool) -> Void) -> Void 16 | let MRMediaRemoteGetNowPlayingApplicationIsPlaying = unsafeBitCast( 17 | MRMediaRemoteGetNowPlayingApplicationIsPlayingPointer, 18 | to: MRMediaRemoteGetNowPlayingApplicationIsPlayingFunction.self 19 | ) 20 | 21 | MRMediaRemoteGetNowPlayingApplicationIsPlaying(DispatchQueue.main) { playing in 22 | guard CommandLine.arguments.count >= 2, CommandLine.arguments[1] == "-q" else { 23 | print(playing) 24 | return 25 | } 26 | 27 | exit(playing ? 0 : 1) 28 | } 29 | 30 | guard CommandLine.arguments.count >= 2, !CommandLine.arguments.contains("-q"), CommandLine.arguments.contains("-v") else { 31 | RunLoop.main.run(until: Date() + 0.1) 32 | exit(0) 33 | } 34 | 35 | let MRMediaRemoteGetNowPlayingInfoPointer = CFBundleGetFunctionPointerForName( 36 | bundle, 37 | "MRMediaRemoteGetNowPlayingInfo" as CFString 38 | )! 39 | typealias MRMediaRemoteGetNowPlayingInfoFunction = @convention(c) (DispatchQueue, @escaping ([String: Any]?) -> Void) -> Void 40 | let MRMediaRemoteGetNowPlayingInfo = unsafeBitCast( 41 | MRMediaRemoteGetNowPlayingInfoPointer, 42 | to: MRMediaRemoteGetNowPlayingInfoFunction.self 43 | ) 44 | 45 | MRMediaRemoteGetNowPlayingInfo(DispatchQueue.main) { info in 46 | guard var info else { 47 | print("No info") 48 | exit(1) 49 | } 50 | 51 | // set kMRMediaRemoteNowPlayingInfoArtworkData to "exists" to avoid crash 52 | if info["kMRMediaRemoteNowPlayingInfoArtworkData"] != nil { 53 | info["kMRMediaRemoteNowPlayingInfoArtworkData"] = "exists" 54 | } 55 | 56 | print(info) 57 | } 58 | 59 | RunLoop.main.run(until: Date() + 0.1) 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Alin Panaitiu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | swiftfiles := $(patsubst %.swift,bin/%,$(wildcard *.swift)) 2 | all: $(swiftfiles) 3 | 4 | MONITOR_COMPILER_FLAGS = \ 5 | -F$$PWD/Headers \ 6 | -F/System/Library/PrivateFrameworks \ 7 | -framework DisplayServices \ 8 | -framework CoreDisplay \ 9 | -framework OSD \ 10 | -framework MonitorPanel \ 11 | -framework SkyLight \ 12 | -import-objc-header Headers/Bridging-Header.h \ 13 | lib/Extensions.swift 14 | 15 | bin/SwapMonitors: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 16 | bin/ToggleHDR: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 17 | bin/RotateDisplay: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 18 | bin/HorizontalMonitorLayout: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 19 | bin/VerticalMonitorLayout: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 20 | bin/MirrorMacBookToMonitor: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 21 | bin/ReferencePreset: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 22 | bin/ApplyColorProfile: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 23 | bin/SetNativeBrightness: SWIFTC_FLAGS=-F$$PWD/Headers -F/System/Library/PrivateFrameworks -framework DisplayServices -import-objc-header Headers/Bridging-Header.h 24 | bin/SetKeyboardBacklight: SWIFTC_FLAGS=-F$$PWD/Headers -F/System/Library/PrivateFrameworks -framework CoreBrightness -import-objc-header Headers/Bridging-Header.h 25 | bin/Screens: SWIFTC_FLAGS=$(MONITOR_COMPILER_FLAGS) 26 | bin/IsNowPlaying: IsNowPlaying.swift 27 | swiftc -target arm64-apple-macos10.15.4 IsNowPlaying.swift -o bin/com.apple.controlcenter.mac-utils.IsNowPlaying-arm64 && \ 28 | swiftc -target x86_64-apple-macos10.15.4 IsNowPlaying.swift -o bin/com.apple.controlcenter.mac-utils.IsNowPlaying-x86 && \ 29 | lipo -create -output bin/com.apple.controlcenter.mac-utils.IsNowPlaying bin/com.apple.controlcenter.mac-utils.IsNowPlaying-* && \ 30 | cp -f bin/com.apple.controlcenter.mac-utils.IsNowPlaying bin/IsNowPlaying && \ 31 | rm -f bin/IsNowPlaying-* bin/com.apple.controlcenter.mac-utils.IsNowPlaying-* && \ 32 | test -z "$$CODESIGN_CERT" || /usr/bin/codesign -fs "$$CODESIGN_CERT" --options runtime --timestamp bin/com.apple.controlcenter.mac-utils.IsNowPlaying bin/IsNowPlaying 33 | bin/%: bin/%-arm64 bin/%-x86 34 | lipo -create -output bin/$* bin/$*-* && \ 35 | test -z "$$CODESIGN_CERT" || /usr/bin/codesign -fs "$$CODESIGN_CERT" --options runtime --timestamp bin/$* 36 | bin/%-arm64: build/%-arm64/main.swift 37 | mkdir -p ./bin 38 | swiftc $(SWIFTC_FLAGS) -target arm64-apple-macos10.15.4 ./build/$*-arm64/main.swift -o bin/$*-arm64 39 | bin/%-x86: build/%-x86/main.swift 40 | mkdir -p ./bin 41 | swiftc $(SWIFTC_FLAGS) -target x86_64-apple-macos10.15.4 ./build/$*-x86/main.swift -o bin/$*-x86 42 | build/%-arm64/main.swift: %.swift 43 | mkdir -p ./build/$*-arm64 || true 44 | cp -f $*.swift ./build/$*-arm64/main.swift 45 | build/%-x86/main.swift: %.swift 46 | mkdir -p ./build/$*-x86 || true 47 | cp -f $*.swift ./build/$*-x86/main.swift 48 | notarize: 49 | @rm bin/*-arm64 bin/*-x86 || true 50 | zip /tmp/bins.zip bin/* 51 | xcrun notarytool submit --progress --wait --keychain-profile Alin /tmp/bins.zip 52 | 53 | watch: BIN= 54 | watch: 55 | rg -u -t swift -t h --files | entr -rs 'echo \n--------\n; make -j$$(nproc) && ./bin/$(BIN)' 56 | 57 | clean: 58 | rm -rf bin 59 | rm -rf build 60 | rm -f /tmp/bins.zip 61 | rm -f bin/*-arm64 62 | rm -f bin/*-x86 63 | -------------------------------------------------------------------------------- /MirrorMacBookToMonitor.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | import Cocoa 3 | import Foundation 4 | 5 | configure { config in 6 | let macBookDisplay = CGMainDisplayID() 7 | guard let firstDisplay = NSScreen.onlineDisplayIDs.first(where: { $0 != macBookDisplay }) else { 8 | print("No external display found") 9 | return false 10 | } 11 | 12 | if CGDisplayIsInMirrorSet(macBookDisplay) != 0 { 13 | print("Disabling mirroring") 14 | CGConfigureDisplayMirrorOfDisplay(config, macBookDisplay, kCGNullDirectDisplay) 15 | } else { 16 | print("Mirroring MacBook contents to the external monitor") 17 | CGConfigureDisplayMirrorOfDisplay(config, firstDisplay, macBookDisplay) 18 | } 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mac-utils 2 | 3 | Small utilities for macOS 4 | 5 | ## Building 6 | 7 | To build the utilities, you need to have Xcode or CommandLine Tools installed. Then, run the following command in the terminal inside the `mac-utils` directory: 8 | 9 | ```sh 10 | make -j$(nproc) 11 | ``` 12 | 13 | ## runfg 14 | 15 | Run any command ensuring that the process won't be sent to the Apple Silicon efficiency cores. 16 | 17 | - [runfg.swift](/runfg.swift) 18 | - [runfg (compiled binary)](/bin/runfg) 19 | 20 | --- 21 | 22 | ## runbg 23 | 24 | Run any command pinned to the Apple Silicon efficiency cores. 25 | 26 | - [runbg.swift](/runbg.swift) 27 | - [runbg (compiled binary)](/bin/runbg) 28 | 29 | --- 30 | 31 | ## VerticalMonitorLayout 32 | 33 | Arrange the external monitor above the MacBook display. 34 | 35 | - [VerticalMonitorLayout.swift](/VerticalMonitorLayout.swift) 36 | - [VerticalMonitorLayout (compiled binary)](/bin/VerticalMonitorLayout) 37 | 38 | [![add to shortcuts button](img/add-to-shortcuts.svg)](https://www.icloud.com/shortcuts/05d718d1f6c24c1493a73f539ddd12a9) 39 | 40 | ![vertical monitor layout in Display preferences](https://files.alinpanaitiu.com/vertical-monitor-layout.png) 41 | 42 | --- 43 | 44 | ## HorizontalMonitorLayout 45 | 46 | Arrange the external monitor to the left or right of the MacBook display. 47 | 48 | - [HorizontalMonitorLayout.swift](/HorizontalMonitorLayout.swift) 49 | - [HorizontalMonitorLayout (compiled binary)](/bin/HorizontalMonitorLayout) 50 | 51 | --- 52 | 53 | ## SwapMonitors 54 | 55 | - [SwapMonitors.swift](/SwapMonitors.swift) 56 | - [SwapMonitors (compiled binary)](/bin/SwapMonitors) 57 | 58 | In a MacBook with 2 monitors setup, swap the external monitors around. 59 | 60 | [![add to shortcuts button](img/add-to-shortcuts.svg)](https://www.icloud.com/shortcuts/3c9f6a71589a4813904973b3ef493c1f) 61 | 62 | ![swap monitor layout in Display preferences](https://files.alinpanaitiu.com/swap-monitor-layout.png) 63 | 64 | --- 65 | 66 | ## MirrorMacBookToMonitor 67 | 68 | In a MacBook with 1 monitor setup, mirror the MacBook display contents to the external monitor. 69 | 70 | - [MirrorMacBookToMonitor.swift](/MirrorMacBookToMonitor.swift) 71 | - [MirrorMacBookToMonitor (compiled binary)](/bin/MirrorMacBookToMonitor) 72 | 73 | [![add to shortcuts button](img/add-to-shortcuts.svg)](https://www.icloud.com/shortcuts/93b2496bd03b4c21886e2322409240cb) 74 | 75 | ![mirrored MacBook in Display preferences](https://files.alinpanaitiu.com/mirror-macbook-to-monitor.png) 76 | 77 | --- 78 | 79 | ## ToggleHDR 80 | 81 | Enable/disable HDR for a monitor where the **High Dynamic Range** checkbox is available in Display Preferences. 82 | 83 | - [ToggleHDR.swift](/ToggleHDR.swift) 84 | - [ToggleHDR (compiled binary)](/bin/ToggleHDR) 85 | 86 | [![add to shortcuts button](img/add-to-shortcuts.svg)](https://www.icloud.com/shortcuts/2f412b6ad9644aaf83e86bd53cb4294e) 87 | 88 | ![hdr checkbox in Display preferences](https://files.lunar.fyi/hdr-toggle-ventura.webp) 89 | 90 | --- 91 | 92 | ## RotateDisplay 93 | 94 | Change rotation of a display from the command line. 95 | 96 | - [RotateDisplay.swift](/RotateDisplay.swift) 97 | - [RotateDisplay (compiled binary)](/bin/RotateDisplay) 98 | 99 | --- 100 | 101 | ## SetNativeBrightness 102 | 103 | Set brightness for Apple native displays from the command line. 104 | 105 | - [SetNativeBrightness.swift](/SetNativeBrightness.swift) 106 | - [SetNativeBrightness (compiled binary)](/bin/SetNativeBrightness) 107 | 108 | Works for the built-in MacBook and iMac screen, and Apple vendored displays like: 109 | 110 | - Studio Display 111 | - Pro Display XDR 112 | - LG Ultrafine for Mac 113 | - LED Cinema 114 | - Thunderbolt Display 115 | 116 | --- 117 | 118 | ## SetKeyboardBacklight 119 | 120 | Set keyboard backlight brightness from the command line. 121 | 122 | - [SetKeyboardBacklight.swift](/SetKeyboardBacklight.swift) 123 | - [SetKeyboardBacklight (compiled binary)](/bin/SetKeyboardBacklight) 124 | 125 | --- 126 | 127 | ## IsNowPlaying 128 | 129 | Prints `true` (or exits with code `0` on `-q`) if the Mac is currently playing any media. 130 | 131 | Prints now playing media information with `-v` if the Mac is currently playing any media. 132 | 133 | - [IsNowPlaying.swift](/IsNowPlaying.swift) 134 | - compiled binary 135 | - [com.apple.controlcenter.mac-utils.IsNowPlaying (for macOS 15.4+)](/bin/com.apple.controlcenter.mac-utils.IsNowPlaying) 136 | - [IsNowPlaying (for macOS 15.3 and older)](/bin/IsNowPlaying) 137 | 138 | > On macOS 15.4 and newer, the compiled binary has the Control Center bundle ID as a prefix to work around a restriction added in that macOS version. 139 | > 140 | > To keep using the `IsNowPlaying` command on macOS 15.4 and newer, you can symlink the binary to `IsNowPlaying`: 141 | > 142 | > ```sh 143 | > ln -s com.apple.controlcenter.mac-utils.IsNowPlaying IsNowPlaying 144 | > ``` 145 | 146 | --- 147 | 148 | ## IsCameraOn 149 | 150 | Prints true (or exits with code 0 on `-q`) if the Mac camera is in use by any application. 151 | 152 | - [IsCameraOn.swift](/IsCameraOn.swift) 153 | - [IsCameraOn (compiled binary)](/bin/IsCameraOn) 154 | 155 | --- 156 | 157 | ## ReferencePreset 158 | 159 | Activate presets for reference monitors like the Pro Display XDR or the MacBook Pro 2021 screen. 160 | 161 | - [ReferencePreset.swift](/ReferencePreset.swift) 162 | - [ReferencePreset (compiled binary)](/bin/ReferencePreset) 163 | 164 | ![reference presets in Display preferences](https://files.alinpanaitiu.com/reference-display-presets.png) 165 | 166 | ## ApplyColorProfile 167 | 168 | Apply `.icc` color profiles to any display. It works even if the display does not have a color profile setting in System Settings. 169 | 170 | - [ApplyColorProfile.swift](/ApplyColorProfile.swift) 171 | - [ApplyColorProfile (compiled binary)](/bin/ApplyColorProfile) 172 | 173 | --- 174 | 175 | ## SendMediaKey 176 | 177 | Send media key events such as play/pause, next, previous, volume up, volume down, mute, etc. 178 | 179 | - [SendMediaKey.swift](/SendMediaKey.swift) 180 | - [SendMediaKey (compiled binary)](/bin/SendMediaKey) 181 | -------------------------------------------------------------------------------- /ReferencePreset.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | func presetStrings(_ display: MPDisplay) -> [String] { 5 | display.presets.filter(\.isValid).map { preset in 6 | "\(preset.presetIndex). \(preset.presetName ?? "NO NAME")" 7 | } 8 | } 9 | 10 | func printDisplays(_ displays: [MPDisplay]) { 11 | for panel in displays { 12 | print(""" 13 | \(panel.displayName ?? "Unknown display") 14 | ID: \(panel.displayID) 15 | UUID: \(panel.uuid?.uuidString ?? "") 16 | Preset: \(panel.activePreset?.presetName ?? "No preset") 17 | Presets: 18 | \t\(presetStrings(panel).joined(separator: "\n\t")) 19 | """) 20 | } 21 | } 22 | 23 | func setReferencePreset(display: MPDisplay, presetFilter: String) { 24 | let displayName = display.displayName ?? "display" 25 | 26 | if let index = Int(presetFilter) { 27 | guard let preset = display.presets.first(where: { $0.presetIndex == index }) else { 28 | print("No preset with index \(index) for \(displayName)") 29 | return 30 | } 31 | print("Activating preset \"\(preset.presetName ?? presetFilter)\" for \(displayName)") 32 | display.setActivePreset(preset) 33 | return 34 | } 35 | 36 | guard let preset = display.presets.first(where: { $0.presetName == presetFilter }) else { 37 | print("No preset with name \(presetFilter) for \(displayName)") 38 | return 39 | } 40 | print("Activating preset \"\(preset.presetName ?? presetFilter)\" for \(displayName)") 41 | display.setActivePreset(preset) 42 | } 43 | 44 | func main() { 45 | guard let mgr = MPDisplayMgr(), let displays = mgr.displays else { 46 | print("No displays") 47 | return 48 | } 49 | 50 | guard CommandLine.arguments.count >= 3 else { 51 | printDisplays(displays) 52 | print("\nUsage: \(CommandLine.arguments[0]) ") 53 | return 54 | } 55 | 56 | defer { 57 | print("") 58 | printDisplays(displays) 59 | } 60 | 61 | let display = CommandLine.arguments[1] 62 | let preset = CommandLine.arguments[2] 63 | 64 | // Example: `ReferencePreset all 2` 65 | if display.lowercased() == "all" { 66 | for display in displays.filter(\.hasPresets) { 67 | setReferencePreset(display: display, presetFilter: preset) 68 | } 69 | return 70 | } 71 | 72 | // Example: `ReferencePreset DELL 2` 73 | guard let display = mgr.matchDisplay(filter: display) else { 74 | print("No display found for query: \(display)") 75 | 76 | return 77 | } 78 | 79 | setReferencePreset(display: display, presetFilter: preset) 80 | } 81 | 82 | main() 83 | -------------------------------------------------------------------------------- /RotateDisplay.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import ColorSync 3 | import Foundation 4 | 5 | func rotateDisplay(display: MPDisplay, orientation: Int32) { 6 | let id = display.displayID 7 | let name = display.name 8 | 9 | guard display.canChangeOrientation() else { 10 | print("\nThe display does not support changing orientation: \(name) [ID: \(id)]") 11 | return 12 | } 13 | 14 | print("\nChanging orientation for \(name) [ID: \(id)]: \(display.orientation) -> \(orientation)") 15 | display.orientation = orientation 16 | } 17 | 18 | func printDisplays(_ displays: [MPDisplay]) { 19 | print("ID\tUUID \tCan Change Orientation\tOrientation\tName") 20 | for panel in displays { 21 | print( 22 | "\(panel.displayID)\t\(panel.uuid?.uuidString ?? "")\t\(panel.canChangeOrientation()) \t\(panel.orientation)° \t\(panel.name)" 23 | ) 24 | } 25 | } 26 | 27 | func main() { 28 | guard let mgr = MPDisplayMgr(), let displays = mgr.displays else { return } 29 | printDisplays(displays) 30 | 31 | guard CommandLine.arguments.count >= 3, let orientation = Int32(CommandLine.arguments[2]), 32 | [0, 90, 180, 270].contains(orientation) 33 | else { 34 | print("\nUsage: \(CommandLine.arguments[0]) 0|90|180|270") 35 | return 36 | } 37 | 38 | let arg = CommandLine.arguments[1] 39 | 40 | guard let display = mgr.matchDisplay(filter: arg) else { 41 | print("\nNo display found for query: \(arg)") 42 | 43 | return 44 | } 45 | 46 | rotateDisplay(display: display, orientation: orientation) 47 | } 48 | 49 | main() 50 | -------------------------------------------------------------------------------- /SendMediaKey.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | func mediaKey(for key: String) -> Int32? { 5 | switch key.lowercased().replacingOccurrences(of: "[^a-z]", with: "", options: .regularExpression) { 6 | case "soundup", "volumeup": 7 | NX_KEYTYPE_SOUND_UP 8 | case "sounddown", "volumedown": 9 | NX_KEYTYPE_SOUND_DOWN 10 | case "play", "pause", "playpause": 11 | NX_KEYTYPE_PLAY 12 | case "next": 13 | NX_KEYTYPE_NEXT 14 | case "previous", "prev": 15 | NX_KEYTYPE_PREVIOUS 16 | case "fast": 17 | NX_KEYTYPE_FAST 18 | case "rewind": 19 | NX_KEYTYPE_REWIND 20 | case "brightnessup": 21 | NX_KEYTYPE_BRIGHTNESS_UP 22 | case "brightnessdown": 23 | NX_KEYTYPE_BRIGHTNESS_DOWN 24 | case "mute": 25 | NX_KEYTYPE_MUTE 26 | default: 27 | nil 28 | } 29 | } 30 | 31 | func printUsage() { 32 | print("Usage: SendMediaKey ") 33 | print("Available keys: volumeUp, volumeDown, playPause, next, previous, fast, rewind, brightnessUp, brightnessDown, mute") 34 | } 35 | 36 | let arguments = CommandLine.arguments 37 | guard arguments.count > 1 && !(arguments.contains("-h") || arguments.contains("--help")) else { 38 | printUsage() 39 | exit(1) 40 | } 41 | 42 | let keyString = arguments[1] 43 | guard let mediaKey = mediaKey(for: keyString) else { 44 | print("Error: Invalid key name '\(keyString)'") 45 | printUsage() 46 | exit(1) 47 | } 48 | 49 | func HIDPostAuxKey(key: Int32) { 50 | func doKey(down: Bool) { 51 | let flags = NSEvent.ModifierFlags(rawValue: down ? 0xA00 : 0xB00) 52 | let data1 = Int((key << 16) | (down ? 0xA00 : 0xB00)) 53 | 54 | let ev = NSEvent.otherEvent( 55 | with: NSEvent.EventType.systemDefined, 56 | location: NSPoint(x: 0, y: 0), 57 | modifierFlags: flags, 58 | timestamp: 0, 59 | windowNumber: 0, 60 | context: nil, 61 | subtype: 8, 62 | data1: data1, 63 | data2: -1 64 | ) 65 | let cev = ev?.cgEvent 66 | cev?.post(tap: CGEventTapLocation.cghidEventTap) 67 | } 68 | doKey(down: true) 69 | doKey(down: false) 70 | } 71 | 72 | HIDPostAuxKey(key: mediaKey) 73 | RunLoop.main.run(until: Date() + 0.1) 74 | -------------------------------------------------------------------------------- /SetKeyboardBacklight.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | guard CommandLine.arguments.count >= 2, let bl = Float(CommandLine.arguments[1]) else { 5 | print("Usage: \(CommandLine.arguments[0]) ") 6 | exit(1) 7 | } 8 | 9 | let kbc = KeyboardBrightnessClient() 10 | kbc.setBrightness(bl, forKeyboard: 1) 11 | -------------------------------------------------------------------------------- /SetNativeBrightness.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | guard CommandLine.arguments.count >= 3, let id = UInt32(CommandLine.arguments[1]), let br = Float(CommandLine.arguments[2]) else { 5 | print("Usage: \(CommandLine.arguments[0]) ") 6 | exit(1) 7 | } 8 | 9 | DisplayServicesSetBrightness(id, br) 10 | -------------------------------------------------------------------------------- /SwapMonitors.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | import Cocoa 3 | import Foundation 4 | 5 | func swap(firstDisplay: CGDirectDisplayID, secondDisplay: CGDirectDisplayID, rotation: Bool = true) { 6 | if let firstName = NSScreen.name(for: firstDisplay), let secondName = NSScreen.name(for: secondDisplay) { 7 | print("Swapping \(firstName) [ID: \(firstDisplay)] with \(secondName) [ID: \(secondDisplay)]\n") 8 | } else { 9 | print("Swapping \(firstDisplay) with \(secondDisplay)\n") 10 | } 11 | 12 | configure { config in 13 | let firstMonitorBounds = CGDisplayBounds(firstDisplay) 14 | let secondMonitorBounds = CGDisplayBounds(secondDisplay) 15 | print( 16 | "External Display 1: x=\(firstMonitorBounds.origin.x) y=\(firstMonitorBounds.origin.y) width=\(firstMonitorBounds.width) height=\(firstMonitorBounds.height)" 17 | ) 18 | print("\tMoving to: x=\(secondMonitorBounds.origin.x) y=\(secondMonitorBounds.origin.y)\n") 19 | print( 20 | "External Display 2: x=\(secondMonitorBounds.origin.x) y=\(secondMonitorBounds.origin.y) width=\(secondMonitorBounds.width) height=\(secondMonitorBounds.height)" 21 | ) 22 | print("\tMoving to: x=\(firstMonitorBounds.origin.x) y=\(firstMonitorBounds.origin.y)\n") 23 | 24 | CGConfigureDisplayOrigin( 25 | config, 26 | firstDisplay, 27 | Int32(secondMonitorBounds.origin.x.rounded()), 28 | Int32(secondMonitorBounds.origin.y.rounded()) 29 | ) 30 | CGConfigureDisplayOrigin( 31 | config, 32 | secondDisplay, 33 | Int32(firstMonitorBounds.origin.x.rounded()), 34 | Int32(firstMonitorBounds.origin.y.rounded()) 35 | ) 36 | return true 37 | } 38 | 39 | guard rotation, let mgr, let displays, 40 | let display1 = displays.first(where: { $0.displayID == firstDisplay }), 41 | let display2 = displays.first(where: { $0.displayID == secondDisplay }) 42 | else { return } 43 | 44 | guard display1.canChangeOrientation(), display2.canChangeOrientation() 45 | else { 46 | print("The monitors don't have the ability to change orientation") 47 | return 48 | } 49 | guard display1.orientation != display2.orientation 50 | else { 51 | print("Orientation is the same for both monitors") 52 | return 53 | } 54 | let rotation1 = display1.orientation 55 | let rotation2 = display2.orientation 56 | 57 | mgr.reconfigure { _ in 58 | print("Swapping orientation for \(display1.displayName ?? firstDisplay.s): \(rotation1) -> \(rotation2)") 59 | display1.orientation = rotation2 60 | print("Swapping orientation for \(display2.displayName ?? secondDisplay.s): \(rotation2) -> \(rotation1)") 61 | display2.orientation = rotation1 62 | } 63 | } 64 | 65 | let mgr = MPDisplayMgr() 66 | let displays = mgr?.displays as? [MPDisplay] 67 | 68 | let builtinDisplay = displays?.first(where: \.isBuiltIn)?.displayID.cg ?? CGMainDisplayID() 69 | let ids = NSScreen.onlineDisplayIDs 70 | let externalIDs = ids.filter { $0 != builtinDisplay } 71 | let screenMapping = [CGDirectDisplayID: NSScreen](uniqueKeysWithValues: ids.compactMap { id in 72 | guard let screen = NSScreen.screen(with: id) else { return nil } 73 | return (id, screen) 74 | }) 75 | let screenGroupsByName = [String: [NSScreen]]( 76 | grouping: NSScreen.screens, by: { screen in 77 | let s = screen.localizedName 78 | return NAME_STRIP_REGEX.stringByReplacingMatches(in: s, range: NSMakeRange(0, s.count), withTemplate: "$1") 79 | } 80 | ) 81 | 82 | func printDisplays(_ displays: [MPDisplay]) { 83 | print("ID\tUUID \tOrientation\tName") 84 | for panel in displays { 85 | print("\(panel.displayID)\t\(panel.uuid?.uuidString ?? "")\t\(panel.orientation)° \t\(panel.displayName ?? "Unknown name")") 86 | } 87 | print("") 88 | } 89 | 90 | let ROTATION_ARG_SET: NSOrderedSet = ["--no-rotation", "--no-orientation", "-nr"] 91 | let PRINT_IDS_ARG_SET: NSOrderedSet = ["ids", "print-ids"] 92 | let HELP_ARG_SET: NSOrderedSet = ["-h", "--help"] 93 | 94 | func main() { 95 | let args = CommandLine.arguments 96 | let argSet = Set(args) 97 | let noSwapRotation = ROTATION_ARG_SET.intersectsSet(argSet) 98 | let arg1 = args.count == 2 ? args[1].lowercased() : "" 99 | 100 | if let displays { 101 | printDisplays(displays) 102 | } 103 | 104 | if args.count == 2, PRINT_IDS_ARG_SET.contains(arg1) || HELP_ARG_SET.contains(arg1) { 105 | if HELP_ARG_SET.contains(arg1) { 106 | print("Usage: \(args[0]) [-nr|--no-rotation] ID1 ID2\n") 107 | print("IDs are optional if there are only 2 monitors with the same name\n") 108 | } 109 | 110 | let sortedIDs = externalIDs.sorted { d1, d2 in 111 | CGDisplayBounds(d1).origin.x < CGDisplayBounds(d2).origin.x 112 | } 113 | 114 | print("IDs of monitors in order of appearance from left to right: \(sortedIDs)") 115 | print("Names of monitors in order of appearance from left to right: \(sortedIDs.map { NSScreen.name(for: $0) ?? "Unknown" })") 116 | 117 | return 118 | } 119 | if args.count >= 3, let first = Int(args[args.count - 2]), let second = Int(args[args.count - 1]) { 120 | let firstDisplay = CGDirectDisplayID(first) 121 | let secondDisplay = CGDirectDisplayID(second) 122 | 123 | guard ids.contains(firstDisplay) else { 124 | print("Display \(firstDisplay) not found") 125 | return 126 | } 127 | guard ids.contains(secondDisplay) else { 128 | print("Display \(secondDisplay) not found") 129 | return 130 | } 131 | swap(firstDisplay: firstDisplay, secondDisplay: secondDisplay, rotation: !noSwapRotation) 132 | return 133 | } 134 | 135 | if let screensWithSameName = screenGroupsByName.values.first(where: { $0.count == 2 }), 136 | let firstDisplay = screensWithSameName[0].displayID, let secondDisplay = screensWithSameName[1].displayID 137 | { 138 | swap(firstDisplay: firstDisplay, secondDisplay: secondDisplay, rotation: !noSwapRotation) 139 | return 140 | } 141 | 142 | if let screensWithSameName = screenGroupsByName.first(where: { $0.value.count > 2 }) { 143 | print("Found \(screensWithSameName.value.count) external displays with the same name: \(screensWithSameName.key)") 144 | 145 | let sortedIDs = screensWithSameName.value.compactMap(\.displayID).sorted { d1, d2 in 146 | CGDisplayBounds(d1).origin.x < CGDisplayBounds(d2).origin.x 147 | } 148 | print("IDs of monitors in order of appearance from left to right: \(sortedIDs)") 149 | 150 | print("\nPass the IDs of the display that you want to swap as arguments") 151 | print("Example: \(args[0]) \(sortedIDs.first!) \(sortedIDs.last!)") 152 | return 153 | } 154 | 155 | guard let firstDisplay = externalIDs.first, 156 | let secondDisplay = externalIDs.first(where: { $0 != firstDisplay }) 157 | else { 158 | print("At least two external displays are needed") 159 | return 160 | } 161 | 162 | swap(firstDisplay: firstDisplay, secondDisplay: secondDisplay, rotation: !noSwapRotation) 163 | } 164 | 165 | main() 166 | -------------------------------------------------------------------------------- /ToggleHDR.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import ColorSync 3 | import Foundation 4 | 5 | let userDefaultsSuiteName = "com.alin23.ToggleHDR" 6 | 7 | func toggleHDR(display: MPDisplay, enabled: Bool? = nil, try60Hz: Bool = false) { 8 | let id = display.displayID 9 | let name = display.displayName ?? "" 10 | 11 | let hdrIsCurrentlyEnabled = display.preferHDRModes() 12 | let newHDRState = enabled ?? !hdrIsCurrentlyEnabled 13 | guard newHDRState != hdrIsCurrentlyEnabled else { 14 | print("HDR is already \(newHDRState ? "enabled" : "disabled") for \(name) [ID: \(id)]") 15 | return 16 | } 17 | 18 | if try60Hz, newHDRState, !display.hasHDRModes, let scanRate = display.currentMode.scanRate, 19 | scanRate.intValue > 60, forceEnableHDR60Hertz(display: display) 20 | { 21 | display.setPreferHDRModes(true) 22 | return 23 | } 24 | 25 | guard display.hasHDRModes else { 26 | print("The display does not support HDR control: \(name) [ID: \(id)]") 27 | return 28 | } 29 | 30 | updatePreferHDR(display: display, enabled: newHDRState) 31 | } 32 | 33 | func printDisplays(_ displays: [MPDisplay]) { 34 | print("ID\tUUID \tSupports HDR\tHDR Enabled\tName") 35 | for panel in displays { 36 | print( 37 | "\(panel.displayID)\t\(panel.uuid?.uuidString ?? "")\t\(panel.hasHDRModes) \t\(panel.preferHDRModes()) \t\(panel.displayName ?? "Unknown name")" 38 | ) 39 | } 40 | } 41 | 42 | func bool(_ arg: String) -> Bool? { 43 | if ["on", "true", "yes"].contains(arg) || arg.starts(with: "enable") { 44 | return true 45 | } 46 | if ["off", "false", "no"].contains(arg) || arg.starts(with: "disable") { 47 | return false 48 | } 49 | return nil 50 | } 51 | 52 | func updatePreferHDR(display: MPDisplay, enabled: Bool) { 53 | let id = display.displayID 54 | let name = display.displayName ?? "" 55 | 56 | print("\(enabled ? "Enabling" : "Disabling") HDR for \(name) [ID: \(id)]") 57 | if !enabled, let ud = UserDefaults(suiteName: userDefaultsSuiteName) { 58 | // We are currently in HDR mode. Check to see if we have a stored SDR 59 | // mode number for the current display, and if that mode number can 60 | // successfully be used to create an MPDisplayMode. 61 | let uuid = display.uuid?.uuidString ?? name 62 | let udKey = "SDRModeNumber-\(uuid)" 63 | let sdrModeNumber = ud.integer(forKey: udKey).i32 64 | if sdrModeNumber > 0, let newMode = display.mode(withNumber: sdrModeNumber) { 65 | display.setMode(newMode) 66 | ud.removeObject(forKey: udKey) 67 | return 68 | } 69 | } 70 | display.setPreferHDRModes(enabled) 71 | } 72 | 73 | func forceEnableHDR60Hertz(display: MPDisplay) -> Bool { 74 | // macOS doesn't think this display supports HDR; see if it supports HDR when set to 60 Hertz 75 | guard let ud = UserDefaults(suiteName: userDefaultsSuiteName), 76 | let scanRates = display.scanRates, scanRates.contains(where: { $0 == 60 }), 77 | let newMode = display.mode(matchingResolutionOf: display.currentMode, withScanRate: 60) 78 | else { 79 | return false 80 | } 81 | // We found a 60 Hertz version of the current display mode. Before 82 | // we make any changes, store the CURRENT display mode in User 83 | // Defaults so that we can restore it later. 84 | let modeNumber = display.currentMode.modeNumber 85 | let uuid = display.uuid?.uuidString ?? display.displayName ?? "" 86 | ud.setValue(modeNumber, forKey: "SDRModeNumber-\(uuid)") 87 | 88 | // Store the current mode so that if the 60 Hertz mode doesn't support HDR we can switch back. 89 | let originalMode = display.currentMode 90 | 91 | // Now switch to the 60 Hertz version of the current display mode. 92 | display.setMode(newMode) 93 | 94 | guard display.hasHDRModes else { 95 | // Even at 60 Hertz, HDR isn't supported; reset back to the original mode. 96 | display.setMode(originalMode) 97 | return false 98 | } 99 | return true 100 | } 101 | 102 | func printHelp() { 103 | print(""" 104 | Usage: \(CommandLine.arguments[0]) [id/uuid/name/all] [on/off] [--try-60-hz] 105 | 106 | Options: 107 | • id/uuid/name: The display to toggle HDR on/off for. 108 | • all: Toggle HDR on/off for all displays that support it. 109 | • on/off: Enable or disable HDR. If not provided, the opposite of the current state is used. 110 | • --try-60-hz: If the display doesn't support HDR at the current refresh rate, it will try switching to 60 Hertz. 111 | 112 | """) 113 | } 114 | 115 | func main() { 116 | guard let mgr = MPDisplayMgr(), let displays = mgr.displays else { return } 117 | 118 | let try60Hz = CommandLine.arguments.contains("--try-60-hz") 119 | let args = CommandLine.arguments.filter { arg in !["--try-60-hz"].contains(arg) } 120 | 121 | guard args.count >= 2 else { 122 | if displays.count == 1, displays[0].hasHDRModes || try60Hz { 123 | // If there is only one display, toggle the HDR on that display. 124 | toggleHDR(display: displays[0], try60Hz: try60Hz) 125 | return 126 | } 127 | printHelp() 128 | printDisplays(displays) 129 | return 130 | } 131 | 132 | if args.contains("--help") || args.contains("-h") { 133 | printHelp() 134 | printDisplays(displays) 135 | return 136 | } 137 | 138 | defer { 139 | print("") 140 | printDisplays(displays) 141 | } 142 | let arg = args[1].lowercased() 143 | 144 | // Example: `ToggleHDR on` or `ToggleHDR off` 145 | if let enabled = bool(arg) { 146 | for display in displays.filter({ $0.hasHDRModes || try60Hz }) { 147 | toggleHDR(display: display, enabled: enabled, try60Hz: try60Hz) 148 | } 149 | return 150 | } 151 | 152 | let enabled = args.count >= 3 ? bool(args[2].lowercased()) : nil 153 | 154 | // Example: `ToggleHDR all` or `ToggleHDR all on` 155 | if arg == "all" { 156 | for display in displays.filter({ $0.hasHDRModes || try60Hz }) { 157 | toggleHDR(display: display, enabled: enabled, try60Hz: try60Hz) 158 | } 159 | return 160 | } 161 | 162 | // Example: `ToggleHDR DELL` or `ToggleHDR DELL on` 163 | guard let display = mgr.matchDisplay(filter: arg) else { 164 | print("No display found for query: \(arg)") 165 | return 166 | } 167 | 168 | toggleHDR(display: display, enabled: enabled, try60Hz: try60Hz) 169 | } 170 | 171 | main() 172 | -------------------------------------------------------------------------------- /VerticalMonitorLayout.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | import Cocoa 3 | import Foundation 4 | 5 | configure { config in 6 | let mainDisplay = CGMainDisplayID() 7 | var macBookDisplay: CGDirectDisplayID 8 | var externalDisplay: CGDirectDisplayID 9 | if CGDisplayIsBuiltin(mainDisplay) != 0 { 10 | macBookDisplay = mainDisplay 11 | guard let otherDisplay = NSScreen.onlineDisplayIDs.first(where: { $0 != macBookDisplay }) else { 12 | print("No external display detected") 13 | return false 14 | } 15 | externalDisplay = otherDisplay 16 | } else { 17 | externalDisplay = mainDisplay 18 | guard let otherDisplay = NSScreen.onlineDisplayIDs.first(where: { CGDisplayIsBuiltin($0) != 0 }) else { 19 | print("No internal display detected") 20 | return false 21 | } 22 | macBookDisplay = otherDisplay 23 | } 24 | 25 | let macBookBounds = CGDisplayBounds(macBookDisplay) 26 | let monitorBounds = CGDisplayBounds(externalDisplay) 27 | print( 28 | "Main Display: x=\(macBookBounds.origin.x) y=\(macBookBounds.origin.y) width=\(macBookBounds.width) height=\(macBookBounds.height)" 29 | ) 30 | print( 31 | "External Display: x=\(monitorBounds.origin.x) y=\(monitorBounds.origin.y) width=\(monitorBounds.width) height=\(monitorBounds.height)" 32 | ) 33 | 34 | if macBookDisplay == mainDisplay { 35 | let monitorX = (max(monitorBounds.width, macBookBounds.width) - min(monitorBounds.width, macBookBounds.width)) / -2 36 | let monitorY = -monitorBounds.height 37 | 38 | print("\nNew external display coordinates: x=\(monitorX) y=\(monitorY)") 39 | CGConfigureDisplayOrigin(config, externalDisplay, Int32(monitorX.rounded()), Int32(monitorY.rounded())) 40 | } else { 41 | let monitorX = (max(macBookBounds.width, monitorBounds.width) - min(macBookBounds.width, monitorBounds.width)) / 2 42 | let monitorY = monitorBounds.height 43 | 44 | print("\nNew internal display coordinates: x=\(monitorX) y=\(monitorY)") 45 | CGConfigureDisplayOrigin(config, macBookDisplay, Int32(monitorX.rounded()), Int32(monitorY.rounded())) 46 | } 47 | 48 | return true 49 | } 50 | -------------------------------------------------------------------------------- /bin/ApplyColorProfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/ApplyColorProfile -------------------------------------------------------------------------------- /bin/HorizontalMonitorLayout: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/HorizontalMonitorLayout -------------------------------------------------------------------------------- /bin/IsCameraOn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/IsCameraOn -------------------------------------------------------------------------------- /bin/IsNowPlaying: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/IsNowPlaying -------------------------------------------------------------------------------- /bin/MirrorMacBookToMonitor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/MirrorMacBookToMonitor -------------------------------------------------------------------------------- /bin/ReferencePreset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/ReferencePreset -------------------------------------------------------------------------------- /bin/RotateDisplay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/RotateDisplay -------------------------------------------------------------------------------- /bin/SendMediaKey: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/SendMediaKey -------------------------------------------------------------------------------- /bin/SetKeyboardBacklight: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/SetKeyboardBacklight -------------------------------------------------------------------------------- /bin/SetNativeBrightness: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/SetNativeBrightness -------------------------------------------------------------------------------- /bin/SwapMonitors: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/SwapMonitors -------------------------------------------------------------------------------- /bin/ToggleHDR: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/ToggleHDR -------------------------------------------------------------------------------- /bin/VerticalMonitorLayout: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/VerticalMonitorLayout -------------------------------------------------------------------------------- /bin/com.apple.controlcenter.mac-utils.IsNowPlaying: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/com.apple.controlcenter.mac-utils.IsNowPlaying -------------------------------------------------------------------------------- /bin/runbg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/runbg -------------------------------------------------------------------------------- /bin/runfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alin23/mac-utils/30a67ba920c493168cf6a74b154a2e335da9a36f/bin/runfg -------------------------------------------------------------------------------- /img/add-to-shortcuts.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/Extensions.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let NAME_STRIP_REGEX = try! NSRegularExpression(pattern: #"(.+)\s+\(\s*\d+\s*\)\s*$"#) 4 | 5 | func configure(_ action: (CGDisplayConfigRef) -> Bool) { 6 | var configRef: CGDisplayConfigRef? 7 | var err = CGBeginDisplayConfiguration(&configRef) 8 | guard err == .success, let config = configRef else { 9 | print("Error with CGBeginDisplayConfiguration: \(err)") 10 | return 11 | } 12 | 13 | guard action(config) else { 14 | _ = CGCancelDisplayConfiguration(config) 15 | return 16 | } 17 | 18 | err = CGCompleteDisplayConfiguration(config, .permanently) 19 | guard err == .success else { 20 | print("Error with CGCompleteDisplayConfiguration") 21 | _ = CGCancelDisplayConfiguration(config) 22 | return 23 | } 24 | } 25 | 26 | extension NSScreen { 27 | static var onlineDisplayIDs: [CGDirectDisplayID] { 28 | let maxDisplays: UInt32 = 16 29 | var onlineDisplays = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays)) 30 | var displayCount: UInt32 = 0 31 | 32 | let err = CGGetOnlineDisplayList(maxDisplays, &onlineDisplays, &displayCount) 33 | if err != .success { 34 | print("Error on getting online displays: \(err)") 35 | } 36 | 37 | return Array(onlineDisplays.prefix(Int(displayCount))) 38 | } 39 | 40 | static var activeDisplayIDs: [CGDirectDisplayID] { 41 | let maxDisplays: UInt32 = 16 42 | var activeDisplays = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays)) 43 | var displayCount: UInt32 = 0 44 | 45 | let err = CGGetActiveDisplayList(maxDisplays, &activeDisplays, &displayCount) 46 | if err != .success { 47 | print("Error on getting active displays: \(err)") 48 | } 49 | 50 | return Array(activeDisplays.prefix(Int(displayCount))) 51 | } 52 | 53 | static var connectedDisplayIDs: [CGDirectDisplayID] { 54 | let maxDisplays: UInt32 = 16 55 | var connectedDisplays = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays)) 56 | var displayCount: UInt32 = 0 57 | 58 | let err = SLSGetDisplayList(maxDisplays, &connectedDisplays, &displayCount) 59 | if err != .success { 60 | print("Error on getting connected displays: \(err)") 61 | } 62 | 63 | return Array(connectedDisplays.prefix(Int(displayCount))) 64 | } 65 | 66 | var hasMouse: Bool { 67 | let mouseLocation = NSEvent.mouseLocation 68 | if NSMouseInRect(mouseLocation, frame, false) { 69 | return true 70 | } 71 | 72 | guard let event = CGEvent(source: nil) else { 73 | return false 74 | } 75 | 76 | let maxDisplays: UInt32 = 1 77 | var displaysWithCursor = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays)) 78 | var displayCount: UInt32 = 0 79 | 80 | let _ = CGGetDisplaysWithPoint(event.location, maxDisplays, &displaysWithCursor, &displayCount) 81 | guard let id = displaysWithCursor.first else { 82 | return false 83 | } 84 | return id == displayID 85 | } 86 | 87 | static var withMouse: NSScreen? { 88 | screens.first { $0.hasMouse } 89 | } 90 | 91 | var displayID: CGDirectDisplayID? { 92 | guard let id = deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber 93 | else { return nil } 94 | return CGDirectDisplayID(id.uint32Value) 95 | } 96 | 97 | static func name(for id: CGDirectDisplayID) -> String? { 98 | screen(with: id)?.localizedName 99 | } 100 | 101 | static func screen(with id: CGDirectDisplayID) -> NSScreen? { 102 | NSScreen.screens.first { $0.hasDisplayID(id) } 103 | } 104 | 105 | func hasDisplayID(_ id: CGDirectDisplayID) -> Bool { 106 | guard let screenNumber = displayID else { return false } 107 | return id == screenNumber 108 | } 109 | } 110 | 111 | extension MPDisplay { 112 | var name: String { 113 | isSidecarDisplay ? "Sidecar" : (titleName ?? displayName ?? "Unknown name") 114 | } 115 | 116 | var str: String { 117 | """ 118 | "id": \(displayID) 119 | "aliasID": \(aliasID) 120 | "canChangeOrientation": \(canChangeOrientation()) 121 | "hasRotationSensor": \(hasRotationSensor) 122 | "hasZeroRate": \(hasZeroRate) 123 | "hasMultipleRates": \(hasMultipleRates) 124 | "isSidecarDisplay": \(isSidecarDisplay) 125 | "isAirPlayDisplay": \(isAirPlayDisplay) 126 | "isProjector": \(isProjector) 127 | "is4K": \(is4K) 128 | "isTV": \(isTV) 129 | "isMirrorMaster": \(isMirrorMaster) 130 | "isMirrored": \(isMirrored) 131 | "isBuiltIn": \(isBuiltIn) 132 | "isHiDPI": \(isHiDPI) 133 | "hasTVModes": \(hasTVModes) 134 | "hasSimulscan": \(hasSimulscan) 135 | "hasSafeMode": \(hasSafeMode) 136 | "isSmartDisplay": \(isSmartDisplay) 137 | "isAppleProDisplay": \(isAppleProDisplay) 138 | "uuid": \(uuid?.uuidString ?? "") 139 | "isForcedToMirror": \(isForcedToMirror) 140 | "hasMenuBar": \(hasMenuBar) 141 | "isBuiltInRetina": \(isBuiltInRetina) 142 | "titleName": \(titleName ?? "") 143 | "name": \(displayName ?? "") 144 | "orientation": \(orientation) 145 | """ 146 | } 147 | } 148 | 149 | extension MPDisplayMgr { 150 | func matchDisplay(filter: String) -> MPDisplay? { 151 | guard let displays else { return nil } 152 | 153 | if ["cursor", "current", "main"].contains(filter.lowercased()), 154 | let cursorDisplayID = NSScreen.withMouse?.displayID, 155 | let display = displays.first(where: { $0.displayID == cursorDisplayID }) 156 | { 157 | return display 158 | } 159 | if let id = Int(filter), let display = displays.first(where: { $0.displayID == id }) { 160 | return display 161 | } 162 | if let uuid = UUID(uuidString: filter.uppercased()), let display = displays.first(where: { $0.uuid == uuid }) { 163 | return display 164 | } 165 | 166 | let filter = filter.lowercased() 167 | if filter == "sidecar" || filter == "ipad", let display = displays.first(where: \.isSidecarDisplay) { 168 | return display 169 | } 170 | if filter == "builtin" || filter == "built-in", let display = displays.first(where: \.isBuiltIn) { 171 | return display 172 | } 173 | if let display = displays.first(where: { $0.displayName?.lowercased() == filter }) { 174 | return display 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func reconfigure(tries: Int = 10, _ action: (MPDisplayMgr) -> Void) { 181 | guard tryLock(tries: tries) else { 182 | return 183 | } 184 | 185 | notifyWillReconfigure() 186 | action(self) 187 | notifyReconfigure() 188 | unlockAccess() 189 | } 190 | 191 | func tryLock(tries: Int = 10) -> Bool { 192 | for i in 1 ... tries { 193 | if tryLockAccess() { return true } 194 | print("Failed to acquire display manager lock (try: \(i))") 195 | Thread.sleep(forTimeInterval: 0.05) 196 | } 197 | return false 198 | } 199 | } 200 | 201 | extension BinaryInteger { 202 | var s: String { String(self) } 203 | } 204 | 205 | extension Int { 206 | var i32: Int32 { Int32(self) } 207 | } 208 | 209 | extension Int32 { 210 | var cg: CGDirectDisplayID { 211 | CGDirectDisplayID(self) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /mac-utils.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /runbg.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | 3 | // Inspired by: https://eclecticlight.co/2021/09/14/how-to-run-commands-and-scripts-on-efficiency-cores/ 4 | 5 | // Run directly: 6 | // chmod +x runbg.swift 7 | // ./runbg.swift 8 | // 9 | // Compile to static binary: 10 | // swiftc runbg.swift -o runbg 11 | // ./runbg 12 | // 13 | // Or download already compiled binary: 14 | // curl https://files.alinpanaitiu.com/runbg > /usr/local/bin/runbg 15 | // chmod +x /usr/local/bin/runbg 16 | // runbg 17 | 18 | // Usage examples: 19 | // Optimize all images on the desktop: runbg imageoptim ~/Desktop 20 | // Re-encode video with ffmpeg to squeeze more bytes: runbg ffmpeg -i big-video.mp4 smaller-video.mp4 21 | // Compile project in background: runbg make -j 4 22 | 23 | import Foundation 24 | 25 | if CommandLine.arguments.count <= 1 { 26 | print(CommandLine.arguments[0], "executable args...") 27 | exit(1) 28 | } 29 | 30 | let SHELL = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh" 31 | let FM = FileManager() 32 | 33 | @discardableResult func asyncNow(timeout: TimeInterval, _ action: @escaping () -> Void) -> DispatchTimeoutResult { 34 | let task = DispatchWorkItem { action() } 35 | DispatchQueue.global().async(execute: task) 36 | 37 | let result = task.wait(timeout: DispatchTime.now() + timeout) 38 | if result == .timedOut { 39 | task.cancel() 40 | } 41 | 42 | return result 43 | } 44 | 45 | // MARK: - ProcessStatus 46 | 47 | struct ProcessStatus { 48 | var output: Data? 49 | var error: Data? 50 | var success: Bool 51 | 52 | var o: String? { 53 | output?.s?.trimmed 54 | } 55 | 56 | var e: String? { 57 | error?.s?.trimmed 58 | } 59 | } 60 | 61 | func stdout(of process: Process) -> Data? { 62 | let stdout = process.standardOutput as! FileHandle 63 | try? stdout.close() 64 | 65 | guard let path = process.environment?["__swift_stdout"], 66 | let stdoutFile = FileHandle(forReadingAtPath: path) else { return nil } 67 | return try! stdoutFile.readToEnd() 68 | } 69 | 70 | func stderr(of process: Process) -> Data? { 71 | let stderr = process.standardOutput as! FileHandle 72 | try? stderr.close() 73 | 74 | guard let path = process.environment?["__swift_stderr"], 75 | let stderrFile = FileHandle(forReadingAtPath: path) else { return nil } 76 | return try! stderrFile.readToEnd() 77 | } 78 | 79 | func shellProc(_ launchPath: String = "/bin/zsh", args: [String], env: [String: String]? = nil) -> Process? { 80 | let outputDir = try! FM.url( 81 | for: .itemReplacementDirectory, 82 | in: .userDomainMask, 83 | appropriateFor: FM.homeDirectoryForCurrentUser, 84 | create: true 85 | ) 86 | 87 | let stdoutFilePath = outputDir.appendingPathComponent("stdout").path 88 | FM.createFile(atPath: stdoutFilePath, contents: nil, attributes: nil) 89 | 90 | let stderrFilePath = outputDir.appendingPathComponent("stderr").path 91 | FM.createFile(atPath: stderrFilePath, contents: nil, attributes: nil) 92 | 93 | guard let stdoutFile = FileHandle(forWritingAtPath: stdoutFilePath), 94 | let stderrFile = FileHandle(forWritingAtPath: stderrFilePath) 95 | else { 96 | return nil 97 | } 98 | 99 | let task = Process() 100 | task.standardOutput = stdoutFile 101 | task.standardError = stderrFile 102 | task.launchPath = launchPath 103 | task.arguments = args 104 | 105 | var env = env ?? ProcessInfo.processInfo.environment 106 | env["__swift_stdout"] = stdoutFilePath 107 | env["__swift_stderr"] = stderrFilePath 108 | task.environment = env 109 | 110 | do { 111 | try task.run() 112 | } catch { 113 | print("Error running \(launchPath) \(args): \(error)") 114 | return nil 115 | } 116 | 117 | return task 118 | } 119 | 120 | func shell( 121 | _ launchPath: String = "/bin/zsh", 122 | command: String, 123 | timeout: TimeInterval? = nil, 124 | env _: [String: String]? = nil 125 | ) -> ProcessStatus { 126 | shell(launchPath, args: ["-c", command], timeout: timeout) 127 | } 128 | 129 | func shell( 130 | _ launchPath: String = "/bin/zsh", 131 | args: [String], 132 | timeout: TimeInterval? = nil, 133 | env: [String: String]? = nil 134 | ) -> ProcessStatus { 135 | guard let task = shellProc(launchPath, args: args, env: env) else { 136 | return ProcessStatus(output: nil, error: nil, success: false) 137 | } 138 | 139 | guard let timeout else { 140 | task.waitUntilExit() 141 | return ProcessStatus( 142 | output: stdout(of: task), 143 | error: stderr(of: task), 144 | success: task.terminationStatus == 0 145 | ) 146 | } 147 | 148 | let result = asyncNow(timeout: timeout) { 149 | task.waitUntilExit() 150 | } 151 | if result == .timedOut { 152 | task.terminate() 153 | } 154 | 155 | return ProcessStatus( 156 | output: stdout(of: task), 157 | error: stderr(of: task), 158 | success: task.terminationStatus == 0 159 | ) 160 | } 161 | 162 | extension String { 163 | @inline(__always) var trimmed: String { 164 | trimmingCharacters(in: .whitespacesAndNewlines) 165 | } 166 | } 167 | 168 | extension Data { 169 | var s: String? { String(data: self, encoding: .utf8) } 170 | } 171 | 172 | var executable = (CommandLine.arguments[1] as NSString).expandingTildeInPath 173 | if !FM.fileExists(atPath: executable) { 174 | let which = shell(SHELL, command: "which '\(CommandLine.arguments[1])'") 175 | guard which.success, let output = which.o else { 176 | if let err = which.e { 177 | print(err) 178 | } 179 | print("\(executable) not found") 180 | exit(1) 181 | } 182 | executable = output 183 | } 184 | 185 | let p = Process() 186 | p.qualityOfService = .background 187 | p.executableURL = URL(fileURLWithPath: executable) 188 | p.arguments = CommandLine.arguments.suffix(from: 2).map { $0 } 189 | 190 | try! p.run() 191 | 192 | signal(SIGINT) { _ in p.terminate() } 193 | signal(SIGTERM) { _ in p.terminate() } 194 | signal(SIGKILL) { _ in p.terminate() } 195 | 196 | p.waitUntilExit() 197 | 198 | exit(p.terminationStatus) 199 | -------------------------------------------------------------------------------- /runfg.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | 3 | // Run directly: 4 | // chmod +x runfg.swift 5 | // ./runfg.swift 6 | // 7 | // Compile to static binary: 8 | // swiftc runfg.swift -o runfg 9 | // ./runfg 10 | // 11 | // Or download already compiled binary: 12 | // curl https://files.alinpanaitiu.com/runfg > /usr/local/bin/runfg 13 | // chmod +x /usr/local/bin/runfg 14 | // runfg 15 | 16 | // Usage examples: 17 | // Optimize all images on the desktop: runfg imageoptim ~/Desktop 18 | // Re-encode video with ffmpeg to squeeze more bytes: runfg ffmpeg -i big-video.mp4 smaller-video.mp4 19 | // Compile project in background: runfg make -j 4 20 | 21 | import Foundation 22 | 23 | if CommandLine.arguments.count <= 1 { 24 | print(CommandLine.arguments[0], "executable args...") 25 | exit(1) 26 | } 27 | 28 | let SHELL = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh" 29 | let FM = FileManager() 30 | 31 | @discardableResult func asyncNow(timeout: TimeInterval, _ action: @escaping () -> Void) -> DispatchTimeoutResult { 32 | let task = DispatchWorkItem { action() } 33 | DispatchQueue.global().async(execute: task) 34 | 35 | let result = task.wait(timeout: DispatchTime.now() + timeout) 36 | if result == .timedOut { 37 | task.cancel() 38 | } 39 | 40 | return result 41 | } 42 | 43 | // MARK: - ProcessStatus 44 | 45 | struct ProcessStatus { 46 | var output: Data? 47 | var error: Data? 48 | var success: Bool 49 | 50 | var o: String? { 51 | output?.s?.trimmed 52 | } 53 | 54 | var e: String? { 55 | error?.s?.trimmed 56 | } 57 | } 58 | 59 | func stdout(of process: Process) -> Data? { 60 | let stdout = process.standardOutput as! FileHandle 61 | try? stdout.close() 62 | 63 | guard let path = process.environment?["__swift_stdout"], 64 | let stdoutFile = FileHandle(forReadingAtPath: path) else { return nil } 65 | return try! stdoutFile.readToEnd() 66 | } 67 | 68 | func stderr(of process: Process) -> Data? { 69 | let stderr = process.standardOutput as! FileHandle 70 | try? stderr.close() 71 | 72 | guard let path = process.environment?["__swift_stderr"], 73 | let stderrFile = FileHandle(forReadingAtPath: path) else { return nil } 74 | return try! stderrFile.readToEnd() 75 | } 76 | 77 | func shellProc(_ launchPath: String = "/bin/zsh", args: [String], env: [String: String]? = nil) -> Process? { 78 | let outputDir = try! FM.url( 79 | for: .itemReplacementDirectory, 80 | in: .userDomainMask, 81 | appropriateFor: FM.homeDirectoryForCurrentUser, 82 | create: true 83 | ) 84 | 85 | let stdoutFilePath = outputDir.appendingPathComponent("stdout").path 86 | FM.createFile(atPath: stdoutFilePath, contents: nil, attributes: nil) 87 | 88 | let stderrFilePath = outputDir.appendingPathComponent("stderr").path 89 | FM.createFile(atPath: stderrFilePath, contents: nil, attributes: nil) 90 | 91 | guard let stdoutFile = FileHandle(forWritingAtPath: stdoutFilePath), 92 | let stderrFile = FileHandle(forWritingAtPath: stderrFilePath) 93 | else { 94 | return nil 95 | } 96 | 97 | let task = Process() 98 | task.standardOutput = stdoutFile 99 | task.standardError = stderrFile 100 | task.launchPath = launchPath 101 | task.arguments = args 102 | 103 | var env = env ?? ProcessInfo.processInfo.environment 104 | env["__swift_stdout"] = stdoutFilePath 105 | env["__swift_stderr"] = stderrFilePath 106 | task.environment = env 107 | 108 | do { 109 | try task.run() 110 | } catch { 111 | print("Error running \(launchPath) \(args): \(error)") 112 | return nil 113 | } 114 | 115 | return task 116 | } 117 | 118 | func shell( 119 | _ launchPath: String = "/bin/zsh", 120 | command: String, 121 | timeout: TimeInterval? = nil, 122 | env _: [String: String]? = nil 123 | ) -> ProcessStatus { 124 | shell(launchPath, args: ["-c", command], timeout: timeout) 125 | } 126 | 127 | func shell( 128 | _ launchPath: String = "/bin/zsh", 129 | args: [String], 130 | timeout: TimeInterval? = nil, 131 | env: [String: String]? = nil 132 | ) -> ProcessStatus { 133 | guard let task = shellProc(launchPath, args: args, env: env) else { 134 | return ProcessStatus(output: nil, error: nil, success: false) 135 | } 136 | 137 | guard let timeout else { 138 | task.waitUntilExit() 139 | return ProcessStatus( 140 | output: stdout(of: task), 141 | error: stderr(of: task), 142 | success: task.terminationStatus == 0 143 | ) 144 | } 145 | 146 | let result = asyncNow(timeout: timeout) { 147 | task.waitUntilExit() 148 | } 149 | if result == .timedOut { 150 | task.terminate() 151 | } 152 | 153 | return ProcessStatus( 154 | output: stdout(of: task), 155 | error: stderr(of: task), 156 | success: task.terminationStatus == 0 157 | ) 158 | } 159 | 160 | extension String { 161 | @inline(__always) var trimmed: String { 162 | trimmingCharacters(in: .whitespacesAndNewlines) 163 | } 164 | } 165 | 166 | extension Data { 167 | var s: String? { String(data: self, encoding: .utf8) } 168 | } 169 | 170 | var executable = (CommandLine.arguments[1] as NSString).expandingTildeInPath 171 | if !FM.fileExists(atPath: executable) { 172 | let which = shell(SHELL, command: "which '\(CommandLine.arguments[1])'") 173 | guard which.success, let output = which.o else { 174 | if let err = which.e { 175 | print(err) 176 | } 177 | print("\(executable) not found") 178 | exit(1) 179 | } 180 | executable = output 181 | } 182 | 183 | let p = Process() 184 | p.qualityOfService = .userInteractive 185 | p.executableURL = URL(fileURLWithPath: executable) 186 | p.arguments = CommandLine.arguments.suffix(from: 2).map { $0 } 187 | 188 | try! p.run() 189 | 190 | signal(SIGINT) { _ in p.terminate() } 191 | signal(SIGTERM) { _ in p.terminate() } 192 | signal(SIGKILL) { _ in p.terminate() } 193 | 194 | p.waitUntilExit() 195 | 196 | exit(p.terminationStatus) 197 | --------------------------------------------------------------------------------