From 12ecfc38638a731cd922fa9507227f9792517c3a Mon Sep 17 00:00:00 2001 From: arthur Date: Tue, 7 Mar 2023 20:49:42 +0100 Subject: [PATCH] darkdetect --- darkdetect/__init__.py | 44 ++++++++++++ darkdetect/__main__.py | 9 +++ darkdetect/_dummy.py | 19 ++++++ darkdetect/_linux_detect.py | 45 ++++++++++++ darkdetect/_mac_detect.py | 124 ++++++++++++++++++++++++++++++++++ darkdetect/_windows_detect.py | 122 +++++++++++++++++++++++++++++++++ package/tk_app.py | 1 + 7 files changed, 364 insertions(+) create mode 100644 darkdetect/__init__.py create mode 100644 darkdetect/__main__.py create mode 100644 darkdetect/_dummy.py create mode 100644 darkdetect/_linux_detect.py create mode 100644 darkdetect/_mac_detect.py create mode 100644 darkdetect/_windows_detect.py diff --git a/darkdetect/__init__.py b/darkdetect/__init__.py new file mode 100644 index 0000000..73d92de --- /dev/null +++ b/darkdetect/__init__.py @@ -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 diff --git a/darkdetect/__main__.py b/darkdetect/__main__.py new file mode 100644 index 0000000..1cb260b --- /dev/null +++ b/darkdetect/__main__.py @@ -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())) diff --git a/darkdetect/_dummy.py b/darkdetect/_dummy.py new file mode 100644 index 0000000..1e82117 --- /dev/null +++ b/darkdetect/_dummy.py @@ -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() diff --git a/darkdetect/_linux_detect.py b/darkdetect/_linux_detect.py new file mode 100644 index 0000000..0570e6a --- /dev/null +++ b/darkdetect/_linux_detect.py @@ -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') diff --git a/darkdetect/_mac_detect.py b/darkdetect/_mac_detect.py new file mode 100644 index 0000000..8d44bc7 --- /dev/null +++ b/darkdetect/_mac_detect.py @@ -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()) diff --git a/darkdetect/_windows_detect.py b/darkdetect/_windows_detect.py new file mode 100644 index 0000000..2363f18 --- /dev/null +++ b/darkdetect/_windows_detect.py @@ -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') diff --git a/package/tk_app.py b/package/tk_app.py index c092882..e96d138 100644 --- a/package/tk_app.py +++ b/package/tk_app.py @@ -2,6 +2,7 @@ import math import tkinter as tk +import tkinter.font from package.data_structure import Pile_chaine from package.expression import Expression