1
0
mirror of https://github.com/HorlogeSkynet/archey4 synced 2025-05-03 04:00:15 +02:00

[DESKTOP_ENVIRONMENT] Honors environment and adds support for Windows

This patch is actually a complete rework of `WindowManager` entry,
implementing a more complete environment-based detection.
This commit is contained in:
Samuel FORESTIER 2024-04-13 19:41:04 +02:00
parent a0b51b9758
commit e4236800a5
4 changed files with 316 additions and 59 deletions

@ -10,11 +10,13 @@ and this project (partially) adheres to [Semantic Versioning](https://semver.org
- `Model` support for Raspberry Pi 5+
- `none` logo style to completely hide distribution logo
- AppArmor confinement profile (included in Debian and AUR packages)
- `DesktopEnvironment` support for Windows
- `WindowManager` support for Windows
- `WindowManager` support for some Wayland compositors
### Changed
- `Model` honor `/proc/device-tree/model` when it exists
- `DesktopEnvironment` now honors environment (including `XDG_CURRENT_DESKTOP`)
### Removed
- `_distribution` protected attribute from `Output` class

@ -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,

@ -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 @@ DE_DICT = {
"xfce4-session": "Xfce",
}
# From : <https://specifications.freedesktop.org/menu-spec/latest/apb.html>
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 : <https://wiki.archlinux.org/title/Xdg-utils#Environment_variables>
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,17 +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"):
return "Aero"
if windows_release in ("8", "10"):
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.Error):
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

@ -1,16 +1,201 @@
"""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.platform.system",
return_value="Windows",
)
@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.os.getenv",
side_effect=[
"GNOME-Flashback:GNOME", # XDG_CURRENT_DESKTOP
],
)
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.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",
)
@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/emacsdesktop.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
[Desktop Entry]
Comment="Just messing with ConfigParser by adding section and option duplicates"
"""
),
)
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.os.getenv",
side_effect=[
"", # XDG_CURRENT_DESKTOP
"", # DE
"/usr/share/xsessions/foo.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=FooDesktop
"""
),
)
def test_environment_detection_4_bad_desktop_file(self, _) -> None:
"""_environment_detection against legacy `SESSION_DESKTOP` pointing to a desktop file"""
self.assertIsNone(
DesktopEnvironment._environment_detection(), # pylint: disable=protected-access
)
@patch(
"archey.entries.desktop_environment.Processes.list",
@ -20,51 +205,14 @@ class TestDesktopEnvironmentEntry(unittest.TestCase):
"like",
"cinnamon",
"tea",
), # Fake running processes list # Match !
)
def test_match(self, _):
"""Simple list matching"""
self.assertEqual(DesktopEnvironment().value, "Cinnamon")
@patch(
"archey.entries.desktop_environment.Processes.list",
( # Fake running processes list
"do",
"you",
"like",
"unsweetened", # Mismatch...
"coffee",
),
)
@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")
@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.
self.assertEqual(DesktopEnvironment().value, "Aqua")
@patch(
"archey.entries.desktop_environment.Processes.list",
( # Fake running processes list
"do",
"you",
"like",
"unsweetened", # Mismatch...
"coffee",
),
)
@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__":