You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
3.7 KiB

2 years ago
#-----------------------------------------------------------------------------
# Copyright (C) 2019 Alberto Sottile
#
# Distributed under the terms of the 3-clause BSD License.
#-----------------------------------------------------------------------------
import ctypes
import ctypes.util
import subprocess
import sys
import os
from pathlib import Path
from typing import Callable
try:
from Foundation import NSObject, NSKeyValueObservingOptionNew, NSKeyValueChangeNewKey, NSUserDefaults
from PyObjCTools import AppHelper
_can_listen = True
except ModuleNotFoundError:
_can_listen = False
try:
# macOS Big Sur+ use "a built-in dynamic linker cache of all system-provided libraries"
appkit = ctypes.cdll.LoadLibrary('AppKit.framework/AppKit')
objc = ctypes.cdll.LoadLibrary('libobjc.dylib')
except OSError:
# revert to full path for older OS versions and hardened programs
appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit'))
objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc'))
void_p = ctypes.c_void_p
ull = ctypes.c_uint64
objc.objc_getClass.restype = void_p
objc.sel_registerName.restype = void_p
# See https://docs.python.org/3/library/ctypes.html#function-prototypes for arguments description
MSGPROTOTYPE = ctypes.CFUNCTYPE(void_p, void_p, void_p, void_p)
msg = MSGPROTOTYPE(('objc_msgSend', objc), ((1 ,'', None), (1, '', None), (1, '', None)))
def _utf8(s):
if not isinstance(s, bytes):
s = s.encode('utf8')
return s
def n(name):
return objc.sel_registerName(_utf8(name))
def C(classname):
return objc.objc_getClass(_utf8(classname))
def theme():
NSAutoreleasePool = objc.objc_getClass('NSAutoreleasePool')
pool = msg(NSAutoreleasePool, n('alloc'))
pool = msg(pool, n('init'))
NSUserDefaults = C('NSUserDefaults')
stdUserDef = msg(NSUserDefaults, n('standardUserDefaults'))
NSString = C('NSString')
key = msg(NSString, n("stringWithUTF8String:"), _utf8('AppleInterfaceStyle'))
appearanceNS = msg(stdUserDef, n('stringForKey:'), void_p(key))
appearanceC = msg(appearanceNS, n('UTF8String'))
if appearanceC is not None:
out = ctypes.string_at(appearanceC)
else:
out = None
msg(pool, n('release'))
if out is not None:
return out.decode('utf-8')
else:
return 'Light'
def isDark():
return theme() == 'Dark'
def isLight():
return theme() == 'Light'
def _listen_child():
"""
Run by a child process, install an observer and print theme on change
"""
import signal
signal.signal(signal.SIGINT, signal.SIG_IGN)
OBSERVED_KEY = "AppleInterfaceStyle"
class Observer(NSObject):
def observeValueForKeyPath_ofObject_change_context_(
self, path, object, changeDescription, context
):
result = changeDescription[NSKeyValueChangeNewKey]
try:
print(f"{'Light' if result is None else result}", flush=True)
except IOError:
os._exit(1)
observer = Observer.new() # Keep a reference alive after installing
defaults = NSUserDefaults.standardUserDefaults()
defaults.addObserver_forKeyPath_options_context_(
observer, OBSERVED_KEY, NSKeyValueObservingOptionNew, 0
)
AppHelper.runConsoleEventLoop()
def listener(callback: Callable[[str], None]) -> None:
if not _can_listen:
raise NotImplementedError()
with subprocess.Popen(
(sys.executable, "-c", "import _mac_detect as m; m._listen_child()"),
stdout=subprocess.PIPE,
universal_newlines=True,
cwd=Path(__file__).parent,
) as p:
for line in p.stdout:
callback(line.strip())