diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a4bd188..32520b7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,12 @@ and this project (partially) adheres to [Semantic Versioning](https://semver.org ## [Unreleased] ### Added - AppArmor confinement profile (included in Debian and AUR packages) +- `WindowManager` support for Windows - `DesktopEnvironment` support for Windows +### Changed +- `WindowManager` now honors `XDG_CURRENT_DESKTOP` + ## [v4.14.3.0] - 2024-04-06 ### Added - Official Armbian distribution support diff --git a/apparmor.profile b/apparmor.profile index 4ade673f..6b495470 100644 --- a/apparmor.profile +++ b/apparmor.profile @@ -52,6 +52,9 @@ profile archey4 /usr/{,local/}bin/archey{,4} { # [CPU] entry /{,usr/}bin/lscpu PUx, + # [Desktop Environment] entry + /usr/share/xsessions/*.desktop r, + # [Disk] entry /{,usr/}bin/df PUx, diff --git a/archey/entries/desktop_environment.py b/archey/entries/desktop_environment.py index c0f44654..3c5091d4 100644 --- a/archey/entries/desktop_environment.py +++ b/archey/entries/desktop_environment.py @@ -1,12 +1,15 @@ """Desktop environment detection class""" +import configparser import os import platform +import typing +from contextlib import suppress from archey.entry import Entry from archey.processes import Processes -DE_DICT = { +DE_PROCESSES = { "cinnamon": "Cinnamon", "dde-dock": "Deepin", "fur-box-session": "Fur Box", @@ -19,11 +22,43 @@ "xfce4-session": "Xfce", } +# From : +XDG_DESKTOP_NORMALIZATION = { + "DDE": "Deepin", + "ENLIGHTENMENT": "Enlightenment", + "GNOME-CLASSIC": "GNOME Classic", + "GNOME-FLASHBACK": "GNOME Flashback", + "RAZOR": "Razor-qt", + "TDE": "Trinity", + "X-CINNAMON": "Cinnamon", +} + +# (partly) from : +DE_NORMALIZATION = { + "budgie-desktop": "Budgie", + "cinnamon": "Cinnamon", + "deepin": "Deepin", + "enlightenment": "Enlightenment", + "gnome": "Gnome", + "kde": "KDE", + "lumina": "Lumina", + "lxde": "LXDE", + "lxqt": "LXQt", + "mate": "MATE", + "muffin": "Cinnamon", + "trinity": "Trinity", + "xfce session": "Xfce", + "xfce": "Xfce", + "xfce4": "Xfce", + "xfce5": "Xfce", +} + class DesktopEnvironment(Entry): """ - Just iterate over running processes to find a known-entry. - If not, rely on the `XDG_CURRENT_DESKTOP` environment variable. + Return static values for macOS and Windows. + On Linux, use extensive environment variables processing to find known identifiers. + Fallback on running processes to find a known-entry. """ _ICON = "\ue23c" # fae_restore @@ -32,27 +67,86 @@ class DesktopEnvironment(Entry): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.value = ( + self._platform_detection() or self._environment_detection() or self._process_detection() + ) + + @staticmethod + def _platform_detection() -> typing.Optional[str]: # macOS' desktop environment is called "Aqua", # and could not be detected from processes list. if platform.system() == "Darwin": - self.value = "Aqua" - return + return "Aqua" # Same thing for Windows, based on release version. if platform.system() == "Windows": windows_release = platform.win32_ver()[0] if windows_release in ("Vista", "7"): - self.value = "Aero" - return + return "Aero" if windows_release in ("8", "10"): - self.value = "Metro" - return + return "Metro" + + return None + @staticmethod + def _environment_detection() -> ( + typing.Optional[str] + ): # pylint: disable=too-many-return-statements + """Implement same algorithm xdg-utils uses""" + # Honor XDG_CURRENT_DESKTOP (if set) + desktop_identifiers = os.getenv("XDG_CURRENT_DESKTOP", "").split(":") + if desktop_identifiers[0]: + return XDG_DESKTOP_NORMALIZATION.get( + desktop_identifiers[0].upper(), desktop_identifiers[0] + ) + + # Honor known environment-specific variables + if "GNOME_DESKTOP_SESSION_ID" in os.environ: + return "GNOME" + if "HYPRLAND_CMD" in os.environ: + return "Hyprland" + if "KDE_FULL_SESSION" in os.environ: + return "KDE" + if "MATE_DESKTOP_SESSION_ID" in os.environ: + return "MATE" + if "TDE_FULL_SESSION" in os.environ: + return "Trinity" + + # Fallback to (known) "DE"/"DESKTOP_SESSION" legacy environment variables + legacy_de = os.getenv("DE", "").lower() + if legacy_de in DE_NORMALIZATION: + return DE_NORMALIZATION[legacy_de] + + desktop_session = os.getenv("DESKTOP_SESSION") + if desktop_session is not None: + # If DESKTOP_SESSION corresponds to a session's desktop entry path, parse and honor it + with suppress(ValueError, OSError, configparser.NoOptionError): + desktop_file = os.path.realpath(desktop_session) + if ( + os.path.commonprefix([desktop_file, "/usr/share/xsessions"]) + == "/usr/share/xsessions" + ): + # Don't expect anything from .desktop files and parse them in a best-effort way + config = configparser.ConfigParser(allow_no_value=True, strict=False) + with open(desktop_file, encoding="utf-8") as f_desktop_file: + config.read_file(f_desktop_file) + return ( + # Honor `DesktopNames` option with `X-LightDM-DesktopName` as a fallback + config.get("Desktop Entry", "DesktopNames", fallback=None) + or config.get("Desktop Entry", "X-LightDM-DesktopName") + ).split(";")[0] + + # If not or if file couldn't be read, check whether it corresponds to a known identifier + if desktop_session.lower() in DE_NORMALIZATION: + return DE_NORMALIZATION[desktop_session.lower()] + + return None + + @staticmethod + def _process_detection() -> typing.Optional[str]: processes = Processes().list - for de_id, de_name in DE_DICT.items(): + for de_id, de_name in DE_PROCESSES.items(): if de_id in processes: - self.value = de_name - break - else: - # Let's rely on an environment variable if the loop above didn't `break`. - self.value = os.getenv("XDG_CURRENT_DESKTOP") + return de_name + + return None diff --git a/archey/entries/window_manager.py b/archey/entries/window_manager.py index 7211b686..e41fac18 100644 --- a/archey/entries/window_manager.py +++ b/archey/entries/window_manager.py @@ -85,6 +85,8 @@ def __init__(self, *args, **kwargs): else: if platform.system() == "Darwin": name = "Quartz Compositor" + elif platform.system() == "Windows": + name = "Desktop Window Manager" display_server_protocol = DSP_DICT.get(os.getenv("XDG_SESSION_TYPE", "")) diff --git a/archey/test/entries/test_archey_desktop_environment.py b/archey/test/entries/test_archey_desktop_environment.py index e143f75c..41207f95 100644 --- a/archey/test/entries/test_archey_desktop_environment.py +++ b/archey/test/entries/test_archey_desktop_environment.py @@ -1,70 +1,187 @@ """Test module for Archey's desktop environment detection module""" import unittest -from unittest.mock import patch +from unittest.mock import mock_open, patch from archey.entries.desktop_environment import DesktopEnvironment -@patch("archey.entries.desktop_environment.platform.system", return_value="Linux") class TestDesktopEnvironmentEntry(unittest.TestCase): - """ - With the help of a fake running processes list, we test the DE matching. - """ + """DesktopEnvironment test cases""" @patch( - "archey.entries.desktop_environment.Processes.list", - ( - "do", - "you", - "like", - "cinnamon", - "tea", - ), # Fake running processes list # Match ! + "archey.entries.desktop_environment.platform.system", + return_value="Windows", ) - def test_match(self, _): - """Simple list matching""" - self.assertEqual(DesktopEnvironment().value, "Cinnamon") + @patch( + "archey.entries.desktop_environment.platform.win32_ver", + return_value=("10", "10.0.19042", "SP0", "0", "0", "Workstation"), + ) + def test_platform_detection(self, _, __) -> None: + """_platform_detection simple test""" + self.assertEqual( + DesktopEnvironment._platform_detection(), # pylint: disable=protected-access + "Metro", + ) @patch( - "archey.entries.desktop_environment.Processes.list", - ( # Fake running processes list - "do", - "you", - "like", - "unsweetened", # Mismatch... - "coffee", - ), + "archey.entries.desktop_environment.os.getenv", + side_effect=[ + "GNOME-Flashback:GNOME", # XDG_CURRENT_DESKTOP + ], ) - @patch("archey.entries.desktop_environment.os.getenv", return_value="DESKTOP ENVIRONMENT") - def test_mismatch(self, _, __): - """Simple list (mis-)-matching""" - self.assertEqual(DesktopEnvironment().value, "DESKTOP ENVIRONMENT") + def test_environment_detection_1(self, _) -> None: + """_environment_detection XDG_CURRENT_DESKTOP (normalization) test""" + self.assertEqual( + DesktopEnvironment._environment_detection(), # pylint: disable=protected-access + "GNOME Flashback", + ) - @patch("archey.entries.desktop_environment.platform.system") - def test_darwin_aqua_deduction(self, _, platform_system_mock): - """Test "Aqua" deduction on Darwin systems""" - platform_system_mock.return_value = "Darwin" # Override module-wide mocked value. + @patch( + "archey.entries.desktop_environment.os.getenv", + side_effect=[ + "", # XDG_CURRENT_DESKTOP + ], + ) + @patch.dict( + "archey.entries.desktop_environment.os.environ", + { + "GNOME_DESKTOP_SESSION_ID": "this-is-deprecated", + }, + clear=True, + ) + def test_environment_detection_2(self, _) -> None: + """_environment_detection against environment-specific variables""" + self.assertEqual( + DesktopEnvironment._environment_detection(), # pylint: disable=protected-access + "GNOME", + ) - self.assertEqual(DesktopEnvironment().value, "Aqua") + @patch( + "archey.entries.desktop_environment.os.getenv", + side_effect=[ + "", # XDG_CURRENT_DESKTOP + "Xfce Session", # DE + ], + ) + @patch.dict( + "archey.entries.desktop_environment.os.environ", + {}, + clear=True, + ) + def test_environment_detection_3(self, _) -> None: + """_environment_detection against legacy `DE` environment variable""" + self.assertEqual( + DesktopEnvironment._environment_detection(), # pylint: disable=protected-access + "Xfce", + ) + + @patch( + "archey.entries.desktop_environment.os.getenv", + side_effect=[ + "", # XDG_CURRENT_DESKTOP + "", # DE + "lumina", # SESSION_DESKTOP + ], + ) + @patch.dict( + "archey.entries.desktop_environment.os.environ", + {}, + clear=True, + ) + def test_environment_detection_4(self, _) -> None: + """_environment_detection against legacy `SESSION_DESKTOP` environment variable""" + self.assertEqual( + DesktopEnvironment._environment_detection(), # pylint: disable=protected-access + "Lumina", + ) + + @patch( + "archey.entries.desktop_environment.os.getenv", + side_effect=[ + "", # XDG_CURRENT_DESKTOP + "", # DE + "/usr/share/xsessions/retro-home.desktop", # SESSION_DESKTOP + ], + ) + @patch.dict( + "archey.entries.desktop_environment.os.environ", + {}, + clear=True, + ) + @patch( + "archey.entries.desktop_environment.open", + mock_open( + read_data="""\ +[Desktop Entry] +Name=Retro Home +Comment=Your home for retro gaming +Exec=/usr/local/bin/retro-home +TryExec=ludo +Type=Application +DesktopNames=Retro-Home;Ludo; + +no-value-option +""" + ), + ) + def test_environment_detection_4_desktop_file(self, _) -> None: + """_environment_detection against legacy `SESSION_DESKTOP` pointing to a desktop file""" + self.assertEqual( + DesktopEnvironment._environment_detection(), # pylint: disable=protected-access + "Retro-Home", + ) + + @patch( + "archey.entries.desktop_environment.os.getenv", + side_effect=[ + "", # XDG_CURRENT_DESKTOP + "", # DE + "/usr/share/xsessions/retro-home.desktop", # SESSION_DESKTOP + ], + ) + @patch.dict( + "archey.entries.desktop_environment.os.environ", + {}, + clear=True, + ) + @patch( + "archey.entries.desktop_environment.open", + mock_open( + read_data="""\ +[Desktop Entry] +Name=EmacsDesktop +Comment=EmacsDesktop +Exec=/usr/share/xsessions/emacsdesktop.sh +TryExec=emacs +Type=Application +X-LightDM-DesktopName=EmacsDesktop +""" + ), + ) + def test_environment_detection_4_desktop_file_fallback(self, _) -> None: + """_environment_detection against legacy `SESSION_DESKTOP` pointing to a desktop file""" + self.assertEqual( + DesktopEnvironment._environment_detection(), # pylint: disable=protected-access + "EmacsDesktop", + ) @patch( "archey.entries.desktop_environment.Processes.list", - ( # Fake running processes list + ( "do", "you", "like", - "unsweetened", # Mismatch... - "coffee", + "cinnamon", + "tea", ), ) - @patch( - "archey.entries.desktop_environment.os.getenv", - return_value=None, # The environment variable is empty... - ) - def test_non_detection(self, _, __): - """Simple global non-detection""" - self.assertIsNone(DesktopEnvironment().value) + def test_process_detection(self) -> None: + """_process_detection simple test""" + self.assertEqual( + DesktopEnvironment._process_detection(), # pylint: disable=protected-access + "Cinnamon", + ) if __name__ == "__main__":