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