From e29909c8c87c641ee46ac2fedcacc8af9965e864 Mon Sep 17 00:00:00 2001 From: Michael Harvey Date: Fri, 3 Nov 2023 09:21:02 -0700 Subject: [PATCH] add XFCE support XFCE does not use gsettings and was always returning 'Light'. Added a desktop detection function, and added XFCE support to theme() and listener(). Since 'Light' was confusing for non-GTK desktops, an 'Unknown' theme type is added so that application knows detection was unsuccessful and can choose an appropriate response. Added a 'watch' option to __main__ to demonstrate the listener. --- README.md | 4 ++ darkdetect/__main__.py | 15 +++++- darkdetect/_linux_detect.py | 94 +++++++++++++++++++++++++++---------- 3 files changed, 87 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 66f66a2..259feaf 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This package allows to detect if the user is using Dark Mode on: - [macOS 10.14+](https://support.apple.com/en-us/HT208976) - [Windows 10 1607+](https://blogs.windows.com/windowsexperience/2016/08/08/windows-10-tip-personalize-your-pc-by-enabling-the-dark-theme/) - Linux with [a dark GTK theme](https://www.gnome-look.org/browse/cat/135/ord/rating/?tag=dark). +- Linux with [a dark XFCE theme](https://www.xfce-look.org/browse?tag=dark). The main application of this package is to detect the Dark mode from your GUI Python application (Tkinter/wx/pyqt/qt for python (pyside)/...) and apply the needed adjustments to your interface. Darkdetect is particularly useful if your GUI library **does not** provide a public API for this detection (I am looking at you, Qt). In addition, this package does not depend on other modules or packages that are not already included in standard Python distributions. @@ -25,6 +26,8 @@ False ``` It's that easy. +The theme can be either 'Dark' or 'Light'. If the library cannot detect a theme unambiguously, it returns 'Unknown' so that the application can choose an appropriate default behavior. Unknown may also be returned if not using a GTK or XFCE desktop. When the theme is Unknown, both isDark() and isLight() will return False. + You can create a dark mode switch listener daemon thread with `darkdetect.listener` and pass a callback function. The function will be called with string "Dark" or "Light" when the OS switches the dark mode setting. ``` python @@ -61,3 +64,4 @@ pip install darkdetect[macos-listener] - On macOS, detection of the dark menu bar and dock option (available from macOS 10.10) is not supported. - [Details](https://stackoverflow.com/questions/25207077/how-to-detect-if-os-x-is-in-dark-mode) on the detection method used on macOS. - [Details](https://askubuntu.com/questions/1261366/detecting-dark-mode#comment2132694_1261366) on the experimental detection method used on Linux. + diff --git a/darkdetect/__main__.py b/darkdetect/__main__.py index 1cb260b..3106972 100644 --- a/darkdetect/__main__.py +++ b/darkdetect/__main__.py @@ -1,9 +1,20 @@ #----------------------------------------------------------------------------- -# Copyright (C) 2019 Alberto Sottile +# Copyright (C) 2023 Alberto Sottile, Michael Harvey # # Distributed under the terms of the 3-clause BSD License. +# +# Usage: python -m darkdetect [watch] #----------------------------------------------------------------------------- import darkdetect +import sys + +def print_theme(theme): + print('Current theme: {}'.format(theme)) + +print_theme(darkdetect.theme()) + +# demonstrate use of listener +if len(sys.argv) == 2 and sys.argv[1] == 'watch': + darkdetect.listener(print_theme) -print('Current theme: {}'.format(darkdetect.theme())) diff --git a/darkdetect/_linux_detect.py b/darkdetect/_linux_detect.py index 0570e6a..e2f0ac8 100644 --- a/darkdetect/_linux_detect.py +++ b/darkdetect/_linux_detect.py @@ -1,45 +1,91 @@ #----------------------------------------------------------------------------- -# Copyright (C) 2019 Alberto Sottile, Eric Larson +# Copyright (C) 2023 Alberto Sottile, Eric Larson, Michael Harvey # # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- import subprocess +def _run(args): + out = subprocess.run(args, capture_output=True) + return out.stdout.decode() + + +def _desktop(): + """ + Get simplified desktop identifier. There are several variations of the gnome + desktop string. If desktop is not recognized, return empty string. + + :return: GNOME, MATE, KDE, XFCE, Unity, LXDE + """ + import os + desktop = '' + if 'XDG_CURRENT_DESKTOP' in os.environ: + desktop = os.environ['XDG_CURRENT_DESKTOP'] + + # https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-and-desktop-environment-is-running/227669#227669 + if 'GNOME' in desktop: + desktop = 'GNOME' + elif 'Cinnamon' in desktop: + desktop = 'GNOME' + + return desktop + + def theme(): + """ + :return: 'Dark' or 'Light', or 'Unknown' if the theme cannot be unambiguously identified. + """ + theme_name = '' 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() + desktop = _desktop() + + if desktop == 'GNOME': + #Using the freedesktop specifications for checking dark mode + stdout = _run(['gsettings', 'get', 'org.gnome.desktop.interface', 'color-scheme']) + #If not found then trying older gtk-theme method + if len(stdout)<1: + stdout = _run(['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme']) + # we have a string, now remove start and end quote added by gsettings + theme_name = stdout.lower().strip()[1:-1] + elif desktop == 'XFCE': + theme_name = _run(['xfconf-query', '-c', 'xsettings', '-p', '/Net/ThemeName']) 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 'Unknown' + + if '-dark' in theme_name.lower(): return 'Dark' - else: + elif theme_name: return 'Light' + else: + return 'Unknown' + 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') + desktop = _desktop() + + if desktop == 'GNOME': + 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') + elif desktop == 'XFCE': + with subprocess.Popen( + ('xfconf-query', '-m', '-c', 'xsettings', '-p-', '/Net/ThemeName'), + stdout=subprocess.PIPE, + universal_newlines=True, + ): + callback(theme()) +