├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── build.sh ├── customdisplayprofiles ├── requirements-build.txt ├── requirements-dev.txt ├── requirements-pyinstaller.txt ├── requirements.txt └── sample-helper-login-script └── configure_display_profiles.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .venv 3 | build/ 4 | dist-*/ 5 | pkgroot/ 6 | *.pkg 7 | *.pyc 8 | *.spec 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Timothy Sutton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKGVERSION=1.0.0 2 | PKGNAME=customdisplayprofiles 3 | PKGID=com.github.timsutton.customdisplayprofiles 4 | BUILDPATH=build 5 | INSTALLPATH=/usr/local/bin 6 | MUNKI_REPO_SUBDIR=utilities 7 | 8 | all: clean pkg 9 | clean: 10 | rm -f ${BUILDPATH}/${PKGNAME}-${PKGVERSION}.pkg 11 | rm -rf pkgroot 12 | pkg: 13 | mkdir -p ${BUILDPATH} 14 | mkdir -p pkgroot/${INSTALLPATH} 15 | cp customdisplayprofiles pkgroot/${INSTALLPATH} 16 | pkgbuild --root pkgroot --identifier ${PKGID} --version ${PKGVERSION} ${BUILDPATH}/${PKGNAME}-${PKGVERSION}.pkg 17 | munki: 18 | munkiimport --unattended-install --catalog=testing -n --subdirectory ${MUNKI_REPO_SUBDIR} --developer='Tim Sutton' ${BUILDPATH}/${PKGNAME}-${PKGVERSION}.pkg 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # customdisplayprofiles 2 | 3 | This is a simple command-line Python script that can check, set or unset a custom ColorSync ICC profile for a given display. It uses PyObjC and the most current (as of 2013) ColorSync API to do this. 4 | 5 | ## Installation 6 | 7 | You can either **(1)** download a pre-compiled executable, or **(2)** execute the script directly with a Python runtime you have available. 8 | 9 | ### 1. Pre-compiled executable 10 | 11 | Download the latest version from the [Releases](https://github.com/timsutton/customdisplayprofiles/releases/latest) section, make the file executable and run it. The executable contains the necessary runtime and libraries, bundled via [PyInstaller](https://pyinstaller.org/). 12 | 13 | ### 2. Execute the script directly 14 | 15 | Clone or download this repo (or just the single `customdisplayprofiles` script) and execute it directly. You'll also need to have the [PyObjC PyPi package](https://pypi.org/project/pyobjc/) installed and available in your Python runtime's environment: 16 | 17 | `pip3 install pyobjc` 18 | 19 | 20 | ## Usage 21 | 22 | ### Setting a profile 23 | 24 | Use the `set` action to set a profile (as the user running the command) for the main display. 25 | 26 | `customdisplayprofiles set /path/to/profile.icc` 27 | 28 | Use the `--display` option to configure an alternate display. 29 | 30 | `customdisplayprofiles set --display 2 /path/to/profile.icc` 31 | 32 | If you want to get a list of displays with their associated index: 33 | 34 | `customdisplayprofiles displays` 35 | 36 | 37 | ### Configurable user scope 38 | 39 | The `--user-scope` option allows you to define whether the profile will be applied to the "Current" or "Any" user domain, which may allow you set this preference as a default for all users: 40 | 41 | `customdisplayprofiles set --user-scope any /path/to/profile.icc` 42 | 43 | Specifying `any` here requires root privileges, as it will write these preferences to a system-owned location. 44 | 45 | More information on the user preferences system on OS X can be found [here](https://developer.apple.com/library/mac/#documentation/userexperience/Conceptual/PreferencePanes/Concepts/Managing.html) and [here](http://developer.apple.com/library/ios/#DOCUMENTATION/MacOSX/Conceptual/BPRuntimeConfig/Articles/UserPreferences.html). 46 | 47 | 48 | ### Retrieving the current profile 49 | 50 | The full path to an ICC profile can be printed to stdout: 51 | 52 | `customdisplayprofiles current-path` 53 | 54 | This could be useful if you want to check the current setting using an idempotent login script or a configuration framework like Puppet. 55 | 56 | `current-path` will output nothing if there is no profile currently set for that display. 57 | 58 | 59 | ### Full details 60 | 61 | A more complete dictionary of information can be printed with the `info` action: 62 | 63 |
➜ ./customdisplayprofiles info
64 | {
65 | CustomProfiles = {
66 | 1 = "file://localhost/Library/Application%20Support/Adobe/Color/Profiles/SMPTE-C.icc";
67 | };
68 | DeviceClass = mntr;
69 | DeviceDescription = iMac;
70 | DeviceHostScope = kCFPreferencesCurrentHost;
71 | DeviceID = " 00000610-0000-B005-0000-0000042C0140";
72 | DeviceUserScope = kCFPreferencesAnyUser;
73 | FactoryProfiles = {
74 | 1 = {
75 | DeviceModeDescription = iMac;
76 | DeviceProfileURL = "file://localhost/Library/ColorSync/Profiles/Displays/iMac-00000610-0000-B005-0000-0000042C0140.icc";
77 | };
78 | DeviceDefaultProfileID = 1;
79 | };
80 | }
81 |
82 |
83 |
84 | ## Sample wrapper script
85 |
86 | There's a (very simple) example script in the [sample-helper-login-script](https://github.com/timsutton/customdisplayprofiles/blob/master/sample-helper-login-script/configure_display_profiles.sh) folder, which demonstrates how you could wrap this utility in an environment where you don't manage the ICC profiles directly. Someone calibrating a display would only need to drop the profile in a known folder location, indexed by display number, and at login for all users, the desired color profiles are configured for each online display.
87 |
88 |
89 | ## Building a pkg
90 |
91 | You might want to build a pkg to deploy the script to one or more Macs in your environment. To create a pkg so, you can run the `make` command in the repo folder.
92 | The included Makefile will be used to create a package which will install `customdisplayprofiles` in `/usr/local/bin`
93 | If you'd like to install the script at a different path, you can override the default when creating the package with
94 | `make INSTALLPATH=/path/to/installfolder`
95 |
96 | If you're also using munki, there's a `make munki` command to import the package into your munki repository.
97 |
98 | ```
99 | # first run make to create the pkg
100 | make
101 |
102 | # Then, import the package into munki
103 | make MUNKI_REPO_SUBDIR=util munki
104 | ```
105 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script will output a single self-contained executable at
4 | # the output specified to pyinstaller's `--distpath` option
5 |
6 | set -eu -o pipefail
7 |
8 | # Pick a Python distribution to use, we'll use Apple's for now
9 | PYTHON_DIST_BIN=/usr/bin/python3
10 | # PYTHON_DIST_BIN=/opt/homebrew/bin/python3
11 |
12 | # clean and setup
13 | command -v deactivate && deactivate
14 | rm -rf .venv dist-*
15 | "${PYTHON_DIST_BIN}" -m venv .venv
16 | source .venv/bin/activate
17 |
18 | # build virtualenv
19 | pip install -U pip
20 | pip install -r requirements-build.txt
21 |
22 | # build it
23 | pyinstaller \
24 | --clean \
25 | --log-level DEBUG \
26 | --distpath dist-onefile \
27 | --onefile \
28 | customdisplayprofiles
29 |
--------------------------------------------------------------------------------
/customdisplayprofiles:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | #
3 | # customdisplayprofiles
4 | #
5 | # A command-line utility for setting custom ColorSync ICC profiles
6 | # for connected displays.
7 | #
8 | # Copyright 2013 Timothy Sutton
9 | #
10 | # python3 revision for macOS 12.3 written by Jonathon Irons, 3/17/2022
11 |
12 | import os
13 |
14 | # These three modules all come from the PyPI 'pyobjc' package
15 | # This is NOT the Foundation package
16 | import Foundation
17 | # This is NOT the Quartz package
18 | import Quartz
19 | import ColorSync
20 |
21 | import optparse
22 | import sys
23 |
24 | from pprint import pprint
25 |
26 |
27 | def error_exit(msg, code=1):
28 | print(sys.stderr, msg)
29 | sys.exit(code)
30 |
31 |
32 | def verify_profile(profile_url):
33 | # objc still doesn't seem to know about the CFErrorRefs below
34 | profile, create_err = ColorSync.ColorSyncProfileCreateWithURL(profile_url, None)
35 | usable, errors, warnings = ColorSync.ColorSyncProfileVerify(profile, None, None)
36 |
37 | if errors:
38 | print(sys.stderr, "Errors verifying profile:")
39 | print(sys.stderr, errors)
40 | if warnings:
41 | print(sys.stderr, "Warnings verifying profile:")
42 | print(sys.stderr, warnings)
43 | if not usable:
44 | error_exit("Profile could not be verified!")
45 |
46 |
47 | def main():
48 | possible_actions = {
49 | "current-path": "Print path of current custom profile for device, if any",
50 | "displays": "List online displays and their associated numbers",
51 | "info": "Print out dictionary of device info",
52 | "set": "Set custom profile for device",
53 | "unset": "Unset custom profile for device",
54 | }
55 | possible_user_scopes = ["any", "current"]
56 | default_user_scope = 'current'
57 |
58 | usage = """%s