Last active
January 11, 2025 21:27
-
-
Save homebysix/077b373264a101d84a3f8cb48e9bca84 to your computer and use it in GitHub Desktop.
docklib_example.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/local/bin/managed_python3 | |
"""docklib_example.py | |
This example script demonstrates how Mac admins can use the docklib module to | |
manage the default state of Mac users' Docks. The script is meant to run at | |
login in user context using a tool like Outset or a custom LaunchAgent. | |
For details, see: https://www.elliotjordan.com/posts/resilient-docklib/ | |
""" | |
__author__ = "Elliot Jordan" | |
__version__ = "1.0.3" | |
import getpass | |
import logging | |
import os | |
import shutil | |
import socket | |
import subprocess | |
import sys | |
from datetime import datetime | |
from time import sleep | |
from urllib.parse import unquote, urlparse | |
from docklib import Dock | |
# Set up logging config | |
logging.basicConfig( | |
level=logging.INFO, format="%(asctime)s [%(filename)s] %(levelname)s: %(message)s" | |
) | |
def wait_for_dock(max_time=60): | |
"""Wait for Dock to launch. Bail out if we reach max_time seconds.""" | |
curr_time = max_time | |
check_cmd = ["/usr/bin/pgrep", "-qx", "Dock"] | |
# Check every 1 second for the Dock process | |
while subprocess.run(check_cmd, check=False).returncode != 0: | |
if curr_time <= 0: | |
# We reached our max_time | |
logging.error("Dock did not start within %s seconds. Exiting.", max_time) | |
sys.exit(1) | |
elif curr_time % 5 == 0: | |
# Provide status output every 5 seconds | |
logging.info("Waiting up to %d seconds for Dock to start...", curr_time) | |
# Decrement count and wait one second before looping | |
curr_time -= 1 | |
sleep(1) | |
def is_default(dock): | |
"""Return True if the dock is uncustomized from macOS default; False otherwise.""" | |
# List of default Dock items from recent versions of macOS. Sources: | |
# /System/Library/CoreServices/Dock.app/Contents/Resources/default.plist | |
# /System/Library/CoreServices/Dock.app/Contents/Resources/com.apple.dockfixup.plist | |
# https://512pixels.net/projects/aqua-screenshot-library/ | |
# fmt: off | |
apple_default_apps = [ | |
"App Store", "Calendar", "Contacts", "FaceTime", "Finder", "Freeform", | |
"iBooks", "iCal", "iPhone Mirroring", "iTunes", "Keynote", "Launchpad", | |
"Mail", "Maps", "Messages", "Mission Control", "Music", "News", "Notes", | |
"Numbers", "Pages", "Photo Booth", "Photos", "Podcasts", "Reminders", | |
"Safari", "Siri", "System Preferences", "TV", | |
] | |
# fmt: on | |
# Gather a list of default/custom apps for script output | |
apps = {"default": [], "custom": []} | |
for item in dock.items.get("persistent-apps", []): | |
try: | |
# Compare the path, not the label, due to possible localization | |
pathurl = item["tile-data"]["file-data"]["_CFURLString"] | |
path = urlparse(unquote(pathurl)).path.rstrip("/") | |
app_name = os.path.split(path)[-1].replace(".app", "") | |
# Add each app into either "custom" or "default" list | |
if app_name in apple_default_apps: | |
apps["default"].append(app_name) | |
else: | |
apps["custom"].append(app_name) | |
except Exception as err: | |
logging.error("Exception encountered when processing an item:\n%s", item) | |
logging.error("Raising traceback and leaving Dock unchanged...") | |
raise err | |
logging.info("Apple default apps: %s", ", ".join(apps["default"])) | |
logging.info("Custom apps: %s", ", ".join(apps["custom"])) | |
# Dock is default if no custom apps were found | |
return not apps["custom"] | |
def make_backup(): | |
"""Make a backup of the current Dock configuration prior to applying changes.""" | |
dock_plist = os.path.expanduser("~/Library/Preferences/com.apple.dock.plist") | |
backup_dir = os.path.expanduser("~/Library/PretendCo/backup/") | |
if os.path.isfile(dock_plist): | |
logging.info("Making a backup of the current Dock config in %s...", backup_dir) | |
if not os.path.isdir(backup_dir): | |
os.makedirs(backup_dir) | |
datestamp = datetime.strftime(datetime.now(), "%Y-%m-%d %H-%M-%S") | |
shutil.copy( | |
dock_plist, | |
os.path.join(backup_dir, "com.apple.dock (%s).plist" % datestamp), | |
) | |
def main(): | |
"""Main process.""" | |
# Wait maximum 60 seconds for Dock to start | |
wait_for_dock(60) | |
logging.info("Loading current Dock...") | |
dock = Dock() | |
if not is_default(dock): | |
logging.info("Dock appears to be customized already. Exiting.") | |
sys.exit(0) | |
logging.info("Dock is not customized.") | |
hostname = socket.gethostname() | |
logging.info("Hostname: %s", hostname) | |
username = getpass.getuser() | |
logging.info("Current user: %s", username) | |
# Define list of apps and autohide based on hostname pattern | |
if hostname.startswith("mac-build") or username == "build": | |
logging.info("Setting build Dock...") | |
desired_apps = [ | |
"/Applications/Xcode.app", | |
"/System/Applications/Utilities/Activity Monitor.app", | |
"/System/Applications/Utilities/Console.app", | |
"/System/Applications/Utilities/Terminal.app", | |
] | |
elif hostname.startswith("mac-dash") or username == "dashboard": | |
logging.info("Setting dashboard Dock...") | |
desired_apps = [ | |
"/Applications/Google Chrome.app", | |
] | |
dock.autohide = True | |
elif hostname.startswith("mac-av") or username == "av": | |
logging.info("Setting av Dock...") | |
desired_apps = [ | |
"/System/Applications/QuickTime Player.app", | |
"/System/Applications/VLC.app", | |
] | |
dock.autohide = True | |
elif username == "itadmin": | |
logging.info("Setting itadmin Dock...") | |
desired_apps = [ | |
"/System/Applications/Launchpad.app", | |
"/Applications/Google Chrome.app", | |
"/Applications/Malwarebytes.app", | |
"/System/Applications/Utilities/Activity Monitor.app", | |
"/System/Applications/Utilities/Console.app", | |
"/System/Applications/Utilities/Disk Utility.app", | |
"/System/Applications/Utilities/Terminal.app", | |
] | |
else: | |
# Generic Dock configuration for users/hostnames not specified above | |
logging.info("Setting user Dock...") | |
desired_apps = [ | |
"/System/Applications/Launchpad.app", | |
"/Applications/Google Chrome.app", | |
"/Applications/Microsoft Outlook.app", | |
"/Applications/Slack.app", | |
"/Applications/Managed Software Center.app", | |
"/System/Applications/System Preferences.app", | |
] | |
# Set persistent-apps as desired | |
dock.items["persistent-apps"] = [ | |
dock.makeDockAppEntry(x) for x in desired_apps if os.path.isdir(x) | |
] | |
# Back up existing dock before making changes | |
make_backup() | |
logging.info("Saving and relaunching Dock...") | |
dock.save() | |
logging.info("Done.") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment