Browse Source

darkdetect

master
BARRAUX Arthur 2 years ago
parent
commit
12ecfc3863
  1. 44
      darkdetect/__init__.py
  2. 9
      darkdetect/__main__.py
  3. 19
      darkdetect/_dummy.py
  4. 45
      darkdetect/_linux_detect.py
  5. 124
      darkdetect/_mac_detect.py
  6. 122
      darkdetect/_windows_detect.py
  7. 1
      package/tk_app.py

44
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

9
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()))

19
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()

45
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')

124
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())

122
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')

1
package/tk_app.py

@ -2,6 +2,7 @@
import math import math
import tkinter as tk import tkinter as tk
import tkinter.font
from package.data_structure import Pile_chaine from package.data_structure import Pile_chaine
from package.expression import Expression from package.expression import Expression

Loading…
Cancel
Save