BARRAUX Arthur
2 years ago
7 changed files with 364 additions and 0 deletions
@ -0,0 +1,44 @@ |
|||
#----------------------------------------------------------------------------- |
|||
# Copyright (C) 2019 Alberto Sottile |
|||
# |
|||
# Distributed under the terms of the 3-clause BSD License. |
|||
#----------------------------------------------------------------------------- |
|||
|
|||
__version__ = '0.8.0' |
|||
|
|||
import sys |
|||
import platform |
|||
|
|||
def macos_supported_version(): |
|||
sysver = platform.mac_ver()[0] #typically 10.14.2 or 12.3 |
|||
major = int(sysver.split('.')[0]) |
|||
if major < 10: |
|||
return False |
|||
elif major >= 11: |
|||
return True |
|||
else: |
|||
minor = int(sysver.split('.')[1]) |
|||
if minor < 14: |
|||
return False |
|||
else: |
|||
return True |
|||
|
|||
if sys.platform == "darwin": |
|||
if macos_supported_version(): |
|||
from ._mac_detect import * |
|||
else: |
|||
from ._dummy import * |
|||
elif sys.platform == "win32" and platform.release().isdigit() and int(platform.release()) >= 10: |
|||
# Checks if running Windows 10 version 10.0.14393 (Anniversary Update) OR HIGHER. The getwindowsversion method returns a tuple. |
|||
# The third item is the build number that we can use to check if the user has a new enough version of Windows. |
|||
winver = int(platform.version().split('.')[2]) |
|||
if winver >= 14393: |
|||
from ._windows_detect import * |
|||
else: |
|||
from ._dummy import * |
|||
elif sys.platform == "linux": |
|||
from ._linux_detect import * |
|||
else: |
|||
from ._dummy import * |
|||
|
|||
del sys, platform |
@ -0,0 +1,9 @@ |
|||
#----------------------------------------------------------------------------- |
|||
# Copyright (C) 2019 Alberto Sottile |
|||
# |
|||
# Distributed under the terms of the 3-clause BSD License. |
|||
#----------------------------------------------------------------------------- |
|||
|
|||
import darkdetect |
|||
|
|||
print('Current theme: {}'.format(darkdetect.theme())) |
@ -0,0 +1,19 @@ |
|||
#----------------------------------------------------------------------------- |
|||
# Copyright (C) 2019 Alberto Sottile |
|||
# |
|||
# Distributed under the terms of the 3-clause BSD License. |
|||
#----------------------------------------------------------------------------- |
|||
|
|||
import typing |
|||
|
|||
def theme(): |
|||
return None |
|||
|
|||
def isDark(): |
|||
return None |
|||
|
|||
def isLight(): |
|||
return None |
|||
|
|||
def listener(callback: typing.Callable[[str], None]) -> None: |
|||
raise NotImplementedError() |
@ -0,0 +1,45 @@ |
|||
#----------------------------------------------------------------------------- |
|||
# Copyright (C) 2019 Alberto Sottile, Eric Larson |
|||
# |
|||
# Distributed under the terms of the 3-clause BSD License. |
|||
#----------------------------------------------------------------------------- |
|||
|
|||
import subprocess |
|||
|
|||
def theme(): |
|||
try: |
|||
#Using the freedesktop specifications for checking dark mode |
|||
out = subprocess.run( |
|||
['gsettings', 'get', 'org.gnome.desktop.interface', 'color-scheme'], |
|||
capture_output=True) |
|||
stdout = out.stdout.decode() |
|||
#If not found then trying older gtk-theme method |
|||
if len(stdout)<1: |
|||
out = subprocess.run( |
|||
['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'], |
|||
capture_output=True) |
|||
stdout = out.stdout.decode() |
|||
except Exception: |
|||
return 'Light' |
|||
# we have a string, now remove start and end quote |
|||
theme = stdout.lower().strip()[1:-1] |
|||
if '-dark' in theme.lower(): |
|||
return 'Dark' |
|||
else: |
|||
return 'Light' |
|||
|
|||
def isDark(): |
|||
return theme() == 'Dark' |
|||
|
|||
def isLight(): |
|||
return theme() == 'Light' |
|||
|
|||
# def listener(callback: typing.Callable[[str], None]) -> None: |
|||
def listener(callback): |
|||
with subprocess.Popen( |
|||
('gsettings', 'monitor', 'org.gnome.desktop.interface', 'gtk-theme'), |
|||
stdout=subprocess.PIPE, |
|||
universal_newlines=True, |
|||
) as p: |
|||
for line in p.stdout: |
|||
callback('Dark' if '-dark' in line.strip().removeprefix("gtk-theme: '").removesuffix("'").lower() else 'Light') |
@ -0,0 +1,124 @@ |
|||
#----------------------------------------------------------------------------- |
|||
# 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()) |
@ -0,0 +1,122 @@ |
|||
from winreg import HKEY_CURRENT_USER as hkey, QueryValueEx as getSubkeyValue, OpenKey as getKey |
|||
|
|||
import ctypes |
|||
import ctypes.wintypes |
|||
|
|||
advapi32 = ctypes.windll.advapi32 |
|||
|
|||
# LSTATUS RegOpenKeyExA( |
|||
# HKEY hKey, |
|||
# LPCSTR lpSubKey, |
|||
# DWORD ulOptions, |
|||
# REGSAM samDesired, |
|||
# PHKEY phkResult |
|||
# ); |
|||
advapi32.RegOpenKeyExA.argtypes = ( |
|||
ctypes.wintypes.HKEY, |
|||
ctypes.wintypes.LPCSTR, |
|||
ctypes.wintypes.DWORD, |
|||
ctypes.wintypes.DWORD, |
|||
ctypes.POINTER(ctypes.wintypes.HKEY), |
|||
) |
|||
advapi32.RegOpenKeyExA.restype = ctypes.wintypes.LONG |
|||
|
|||
# LSTATUS RegQueryValueExA( |
|||
# HKEY hKey, |
|||
# LPCSTR lpValueName, |
|||
# LPDWORD lpReserved, |
|||
# LPDWORD lpType, |
|||
# LPBYTE lpData, |
|||
# LPDWORD lpcbData |
|||
# ); |
|||
advapi32.RegQueryValueExA.argtypes = ( |
|||
ctypes.wintypes.HKEY, |
|||
ctypes.wintypes.LPCSTR, |
|||
ctypes.wintypes.LPDWORD, |
|||
ctypes.wintypes.LPDWORD, |
|||
ctypes.wintypes.LPBYTE, |
|||
ctypes.wintypes.LPDWORD, |
|||
) |
|||
advapi32.RegQueryValueExA.restype = ctypes.wintypes.LONG |
|||
|
|||
# LSTATUS RegNotifyChangeKeyValue( |
|||
# HKEY hKey, |
|||
# WINBOOL bWatchSubtree, |
|||
# DWORD dwNotifyFilter, |
|||
# HANDLE hEvent, |
|||
# WINBOOL fAsynchronous |
|||
# ); |
|||
advapi32.RegNotifyChangeKeyValue.argtypes = ( |
|||
ctypes.wintypes.HKEY, |
|||
ctypes.wintypes.BOOL, |
|||
ctypes.wintypes.DWORD, |
|||
ctypes.wintypes.HANDLE, |
|||
ctypes.wintypes.BOOL, |
|||
) |
|||
advapi32.RegNotifyChangeKeyValue.restype = ctypes.wintypes.LONG |
|||
|
|||
def theme(): |
|||
""" Uses the Windows Registry to detect if the user is using Dark Mode """ |
|||
# Registry will return 0 if Windows is in Dark Mode and 1 if Windows is in Light Mode. This dictionary converts that output into the text that the program is expecting. |
|||
valueMeaning = {0: "Dark", 1: "Light"} |
|||
# In HKEY_CURRENT_USER, get the Personalisation Key. |
|||
try: |
|||
key = getKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") |
|||
# In the Personalisation Key, get the AppsUseLightTheme subkey. This returns a tuple. |
|||
# The first item in the tuple is the result we want (0 or 1 indicating Dark Mode or Light Mode); the other value is the type of subkey e.g. DWORD, QWORD, String, etc. |
|||
subkey = getSubkeyValue(key, "AppsUseLightTheme")[0] |
|||
except FileNotFoundError: |
|||
# some headless Windows instances (e.g. GitHub Actions or Docker images) do not have this key |
|||
return None |
|||
return valueMeaning[subkey] |
|||
|
|||
def isDark(): |
|||
if theme() is not None: |
|||
return theme() == 'Dark' |
|||
|
|||
def isLight(): |
|||
if theme() is not None: |
|||
return theme() == 'Light' |
|||
|
|||
#def listener(callback: typing.Callable[[str], None]) -> None: |
|||
def listener(callback): |
|||
hKey = ctypes.wintypes.HKEY() |
|||
advapi32.RegOpenKeyExA( |
|||
ctypes.wintypes.HKEY(0x80000001), # HKEY_CURRENT_USER |
|||
ctypes.wintypes.LPCSTR(b'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize'), |
|||
ctypes.wintypes.DWORD(), |
|||
ctypes.wintypes.DWORD(0x00020019), # KEY_READ |
|||
ctypes.byref(hKey), |
|||
) |
|||
|
|||
dwSize = ctypes.wintypes.DWORD(ctypes.sizeof(ctypes.wintypes.DWORD)) |
|||
queryValueLast = ctypes.wintypes.DWORD() |
|||
queryValue = ctypes.wintypes.DWORD() |
|||
advapi32.RegQueryValueExA( |
|||
hKey, |
|||
ctypes.wintypes.LPCSTR(b'AppsUseLightTheme'), |
|||
ctypes.wintypes.LPDWORD(), |
|||
ctypes.wintypes.LPDWORD(), |
|||
ctypes.cast(ctypes.byref(queryValueLast), ctypes.wintypes.LPBYTE), |
|||
ctypes.byref(dwSize), |
|||
) |
|||
|
|||
while True: |
|||
advapi32.RegNotifyChangeKeyValue( |
|||
hKey, |
|||
ctypes.wintypes.BOOL(True), |
|||
ctypes.wintypes.DWORD(0x00000004), # REG_NOTIFY_CHANGE_LAST_SET |
|||
ctypes.wintypes.HANDLE(None), |
|||
ctypes.wintypes.BOOL(False), |
|||
) |
|||
advapi32.RegQueryValueExA( |
|||
hKey, |
|||
ctypes.wintypes.LPCSTR(b'AppsUseLightTheme'), |
|||
ctypes.wintypes.LPDWORD(), |
|||
ctypes.wintypes.LPDWORD(), |
|||
ctypes.cast(ctypes.byref(queryValue), ctypes.wintypes.LPBYTE), |
|||
ctypes.byref(dwSize), |
|||
) |
|||
if queryValueLast.value != queryValue.value: |
|||
queryValueLast.value = queryValue.value |
|||
callback('Light' if queryValue.value else 'Dark') |
Loading…
Reference in new issue