forked from timsutton/customdisplayprofiles
-
Notifications
You must be signed in to change notification settings - Fork 0
/
customdisplayprofiles
executable file
·188 lines (154 loc) · 6.95 KB
/
customdisplayprofiles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#!/usr/bin/env python3
#
# customdisplayprofiles
#
# A command-line utility for setting custom ColorSync ICC profiles
# for connected displays.
#
# Copyright 2013 Timothy Sutton
#
# python3 revision for macOS 12.3 written by Jonathon Irons, 3/17/2022
import os
# These three modules all come from the PyPI 'pyobjc' package
# This is NOT the Foundation package
import Foundation
# This is NOT the Quartz package
import Quartz
import ColorSync
import optparse
import sys
from pprint import pprint
def error_exit(msg, code=1):
print(sys.stderr, msg)
sys.exit(code)
def verify_profile(profile_url):
# objc still doesn't seem to know about the CFErrorRefs below
profile, create_err = ColorSync.ColorSyncProfileCreateWithURL(profile_url, None)
usable, errors, warnings = ColorSync.ColorSyncProfileVerify(profile, None, None)
if errors:
print(sys.stderr, "Errors verifying profile:")
print(sys.stderr, errors)
if warnings:
print(sys.stderr, "Warnings verifying profile:")
print(sys.stderr, warnings)
if not usable:
error_exit("Profile could not be verified!")
def main():
possible_actions = {
"current-path": "Print path of current custom profile for device, if any",
"displays": "List online displays and their associated numbers",
"info": "Print out dictionary of device info",
"set": "Set custom profile for device",
"unset": "Unset custom profile for device",
}
possible_user_scopes = ["any", "current"]
default_user_scope = 'current'
usage = """%s <action> [options] [/path/to/profile]
Available actions:
""" % (os.path.basename(__file__))
for k, v in possible_actions.items():
usage += " {:<20}{}\n".format(k, v)
o = optparse.OptionParser(usage=usage)
o.add_option('-d', '--display', type='int', default=1,
help="Display number to target the action given. A value of '1' "
"is the default, and means the main display. Second display "
"is '2', etc. Verify the numbers using the 'displays' action.")
o.add_option('-u', '--user-scope', default=default_user_scope,
help="User scope in which to apply the custom profile, when used with the "
"'set' action. Either 'any' or 'current'. 'any' requires "
"root privileges. Defaults to %s."
% default_user_scope)
opts, args = o.parse_args()
if len(args) == 0:
o.print_help()
sys.exit(1)
if opts.user_scope:
if opts.user_scope not in possible_user_scopes:
error_exit("--user-scope must be one of: %s" % ", ".join(possible_user_scopes))
if opts.user_scope == 'any' and os.getuid() != 0:
error_exit("You must have root privileges to modify the any-user scope!")
if args[0] not in possible_actions.keys():
o.print_help()
sys.exit(1)
chosen_action = args[0]
max_displays = 8
# display list retrieval borrowed from Greg Neagle's mirrortool.py
# https://gist.github.com/gregneagle/5722568
(err, display_ids,
number_of_online_displays) = Quartz.CGGetOnlineDisplayList(
max_displays, None, None)
if err:
error_exit("Error in obtaining online display list: %s" % err)
# validate --display option
invalid_display_id = False
invalid_msg = ""
if opts.display <= 0:
invalid_display_id = True
invalid_msg = "Display IDs start at 1."
if opts.display > number_of_online_displays:
invalid_display_id = True
if number_of_online_displays == 1:
invalid_msg = "There is only one display online."
else:
invalid_msg = "There are only %s displays online." % number_of_online_displays
if invalid_display_id:
msg = "--display %s is not valid. " % opts.display
msg += invalid_msg
error_exit(msg)
# Some logic to ensure the first display is always the main one, but
# might confuse things if >2 displays connected.
# main_display_id = Quartz.CGMainDisplayID()
# if number_of_online_displays == 2:
# # main display always seems to be the first ID, but in the opposite
# # case, swap them so it's the first
# if display_ids[1] == main_display_id:
# temp = display_ids[0]
# display_ids[0] = main_display_id
# display_ids[1] = temp
displays = []
for index, display_id in enumerate(display_ids):
display = {
'id': display_id,
'human_id': index + 1,
'device_info': ColorSync.ColorSyncDeviceCopyDeviceInfo(
ColorSync.kColorSyncDisplayDeviceClass, ColorSync.CGDisplayCreateUUIDFromDisplayID(display_id))
}
displays.append(display)
target_display = displays[opts.display - 1]
if chosen_action == 'displays':
for display in displays:
print("%s: %s" % (display['human_id'], display['device_info']['DeviceDescription']))
if chosen_action == "current-path":
if 'CustomProfiles' in target_display['device_info'].keys():
current_profile_url = target_display['device_info']['CustomProfiles']['1']
print(Foundation.CFURLCopyFileSystemPath(current_profile_url, Foundation.kCFURLPOSIXPathStyle))
if chosen_action == "info":
pprint(target_display['device_info'])
if chosen_action in ["set", "unset"]:
if chosen_action == "unset":
profile_url = Foundation.kCFNull
else:
if len(args) < 2:
error_exit("The 'set' action requires a path to an ICC profile as an argument.")
profile_path = args[1]
if not os.path.exists(profile_path):
sys.exit("Can't locate profile at path %s!" % profile_path)
if os.path.isdir(profile_path):
error_exit("'%s' is a directory, not a profile!" % profile_path)
profile_url = Foundation.CFURLCreateFromFileSystemRepresentation(None, profile_path.encode(),
len(profile_path), False)
verify_profile(profile_url)
user_scope = eval('Foundation.kCFPreferences%sUser' % opts.user_scope.capitalize())
# info on config dict required:
# /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSync.framework/Versions/A/Headers/ColorSyncDevice.h
# http://web.archiveorange.com/archive/print/YwdQZYJTswvvG79VTuyD
new_profile_dict = {ColorSync.kColorSyncDeviceDefaultProfileID: profile_url,
ColorSync.kColorSyncProfileUserScope: user_scope}
success = ColorSync.ColorSyncDeviceSetCustomProfiles(
ColorSync.kColorSyncDisplayDeviceClass,
target_display['device_info']['DeviceID'],
new_profile_dict)
if not success:
error_exit("Setting custom profile was unsuccessful!")
if __name__ == '__main__':
main()