Python-Tk: Ein Neofetch-GUI selbst schreiben Teil 11

  Actionschnitzel   Lesezeit: 16 Minuten  🗪 6 Kommentare Auf Mastodon ansehen

Der 11. Teil einer Einführung in die Python3-Bibliothek Tkinter und der lange Weg zum perfekten GUI.

python-tk: ein neofetch-gui selbst schreiben teil 11

Zwischen diesem und dem letzten Teil ist leider etwas mehr als eine Woche vergangen. Das liegt daran, dass der Code immer komplexer wird. Heute soll es um das Erkennen des Desktop Environments gehen. Genau deswegen hat sich alles auch etwas verzögert, denn denken dauert manchmal.

Wo ist das Problem?

Was da so schwer sein soll, ist schnell erklärt. Geht einfach mal im Kopf durch wie viel Desktop-Umgebungen ihr kennt und dann nehmt eine Suchmaschine zur Hand und guckt wie viele es tatsächlich gibt.

Auf Anhieb fällt mir ein: Gnome, KDE, Mate, Cinnamon, Xfce, PIXEL(Raspberry Pi), LXDE, LXQT

All diese Desktops sind auf eine andere Art und Weise im System registriert. Schauen wir doch mal bei Neofetch auf Github vorbei. Genauer: Ab Zeile 1771 finden wir die Funktion get_de(). Ab der Zeile 1811 wird definiert, wie Linux-Desktops ausgelesen werden sollen.

Um mehr über unser System zu erfahren, haben wir schon das Modul os.environment kennengelernt. Suchen wir hier nach dem Begriff "Desktop" so finden wir drei Einträge: 

XDG_CURRENT_DESKTOP, XDG_SESSION_DESKTOP, DESKTOP_SESSION

~$ python3
Python 3.12.3 (main, Jul 31 2024, 17:43:48) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> print(os.environ)

Output

environ({'SHELL': '/bin/bash', 'SESSION_MANAGER': 'local/test-desktop-pi5:@/tmp/.ICE-unix/3553,unix/test-desktop-pi5:/tmp/.ICE-unix/3553', 'QT_ACCESSIBILITY': '1', 'COLORTERM': 'truecolor', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/etc/xdg', 'XDG_MENU_PREFIX': 'gnome-', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'LANGUAGE': 'de_DE:en', 'LC_ADDRESS': 'de_DE.UTF-8', 'GNOME_SHELL_SESSION_MODE': 'ubuntu', 'LC_NAME': 'de_DE.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1002/keyring/ssh', 'MEMORY_PRESSURE_WRITE': 'c29tZSAyMDAwMDAgMjAwMDAwMAA=', 'XMODIFIERS': '@im=ibus', 'DESKTOP_SESSION': 'ubuntu', 'LC_MONETARY': 'de_DE.UTF-8', 'GTK_MODULES': 'gail:atk-bridge', 'DBUS_STARTER_BUS_TYPE': 'session', 'PWD': '/home/test', 'LOGNAME': 'test', 'XDG_SESSION_DESKTOP': 'ubuntu', 'XDG_SESSION_TYPE': 'wayland', 'SYSTEMD_EXEC_PID': '3553', 'XAUTHORITY': '/run/user/1002/.mutter-Xwaylandauth.WWECT2', 'HOME': '/home/test', 'USERNAME': 'test', 'IM_CONFIG_PHASE': '1', 'LC_PAPER': 'de_DE.UTF-8', 'LANG': 'de_DE.UTF-8', 'XDG_CURRENT_DESKTOP': 'ubuntu:GNOME', 'MEMORY_PRESSURE_WATCH': '/sys/fs/cgroup/user.slice/user-1002.slice/user@1002.service/app.slice/app-gnome\\x2dsession\\x2dmanager.slice/gnome-session-manager@ubuntu.service/memory.pressure', 'VTE_VERSION': '7600', 'WAYLAND_DISPLAY': 'wayland-0', 'INVOCATION_ID': '3d44dbaaba694451b24777b735796027', 'QTWEBENGINE_DICTIONARIES_PATH': '/usr/share/hunspell-bdic/', 'MANAGERPID': '3301', 'GNOME_SETUP_DISPLAY': ':1', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'XDG_SESSION_CLASS': 'user', 'TERM': 'xterm-256color', 'LC_IDENTIFICATION': 'de_DE.UTF-8', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'USER': 'test', 'DISPLAY': ':0', 'SHLVL': '1', 'GSM_SKIP_SSH_AGENT_WORKAROUND': 'true', 'LC_TELEPHONE': 'de_DE.UTF-8', 'QT_IM_MODULE': 'ibus', 'LC_MEASUREMENT': 'de_DE.UTF-8', 'DBUS_STARTER_ADDRESS': 'unix:path=/run/user/1002/bus,guid=c22ff578dbe9a3215dd7d07166d8266e', 'PAPERSIZE': 'a4', 'TILIX_ID': 'bdc945c3-3564-4ece-9789-2086791869a9', 'XDG_RUNTIME_DIR': '/run/user/1002', 'DEBUGINFOD_URLS': 'https://debuginfod.ubuntu.com ', 'LC_TIME': 'de_DE.UTF-8', 'JOURNAL_STREAM': '8:21961'

Anmerkung: Daten und Zahlen wurden, für die Privatsphäre, verfälscht.

Alle Desktop-Umgebung auf diese Keys zu checken würde sehr lange dauern, weshalb ich mich nur auf eine Auswahl beschränke. Es zeichnet sich folgendes Bild

Cinnamon Mate XFCE KDE GNOME UBUNTU
XDG_CURRENT_DESKTOP x-cinnamon MATE XFCE KDE GNOME ubuntu:GNOME
XDG_SESSION_DESKTOP cinnamon mate lghtdm-x-session KDE gnome ubuntu
DESKTOP_SESSION cinnamon mate lghtdm-x-session plasma gnome ubuntu

Die richte Wahl wäre hier XDG_CURRENT_DESKTOP, welches Auskunft über den derzeitig benutzen Desktop gibt. Bemerkenswert wären hier zum einen, dass Value x-cinnamon, welches zusätzlich darauf hinweist, dass wir hier die X11 Sitzung laufen haben. Zum anderen wäre da noch ubuntu:GNOME, was uns zeigt, dass Ubuntu-Desktop in der Registrierung von Vannilla-Gnome abweicht.

Wie gehen wir mit diesen Informationen um?

Zunächst einmal bauen wir eine Funktion:

import os

def get_desktop_environment():
    xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP")

    return xdg_current_desktop

print(get_desktop_environment())

Output:

>>> %Run get_de.py
ubuntu:GNOME

Dass es sich bei dem OS um Ubuntu handelt und wird schon über das Label OS: ausgelesen, und aus unserer Tabelle wissen wir, dass Cinnamon mit dem Zusatz x- daher kommt. Wie schon erwähnt, hat jeder Desktop seine Eigenheiten und deswegen werde ich hier nur exemplarisch zeigen, wie man den Output anpasst. Somit könnt Ihr bei Abweichungen auch selbst aktiv werden.

import os

def get_desktop_environment():
    xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP")

    if xdg_current_desktop == "x-cinnamon":
        return "CINNAMON"
    elif xdg_current_desktop == "ubuntu:GNOME":
        return "GNOME"
    else:
        return xdg_current_desktop

print(get_desktop_environment())

In main.py implementieren

def get_desktop_environment():
    xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP")

    if xdg_current_desktop == "x-cinnamon":
        return "CINNAMON"
    elif xdg_current_desktop == "ubuntu:GNOME":
        return "GNOME"
    else:
        return xdg_current_desktop

Logik

  • xdg_current_desktop gibt einen Wert aus
  • Ist dieser Wer x-cinnamon soll CINNAMON zurückgegeben werden
  • Ist dieser Wer ubuntu_GNOME soll GNOME zurückgegeben werden
  • Trift nichts zu, wird der Wert unverfälscht zurückgegeben

Wie gewohnt erstellen wir ein Label:

# Label mit Text DE:
de_label = tk.Label(stat_frame,text=f"DE: {get_desktop_environment()}",background="#FFFFFF",font=("Sans",14))
de_label.pack(anchor=tk.NW)

Hier sei nochmal erwähnt, dass dieser Code natürlich fehleranfällig ist. Ich habe 2 Stunden auf DistroSea verbracht, um diese Eigenheiten zu erfassen. Alle Desktops zu checken, würde als Einzelperson sehr lange dauern. Sollte euer Desktop-Ergebens abweichen könnt ihr gerne den Output in die Kommentare posten und ich passe den Code wie bei Cinnamon und Ubuntu-Gnome an.

Der ganze Code

## Teil 11 ###

import tkinter as tk
from PIL import Image, ImageTk
import os
import socket
import distro
import platform
import psutil
import datetime
import subprocess

def get_desktop_environment():
    xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP")

    if xdg_current_desktop == "x-cinnamon":
        return "CINNAMON"
    elif xdg_current_desktop == "ubuntu:GNOME":
        return "GNOME"
    else:
        return xdg_current_desktop

# Ließt den Window-Manager aus
def get_window_manager_name():
    try:
        result = subprocess.run(
            ["wmctrl", "-m"], capture_output=True, text=True, check=True
        )

        output_lines = result.stdout.strip().split("\n")
        for line in output_lines:
            if line.startswith("Name: "):
                window_manager_name = line.split("Name: ")[1]
                if window_manager_name == "GNOME Shell":
                    return "Mutter"
                return window_manager_name
    except subprocess.CalledProcessError as e:
        print(f"Error running wmctrl: {e}")

# Macht die RAM-Größe lesbar
def get_size(bytes, suffix="B"):
    """
    Scale bytes to its proper format
    e.g:
        1253656 => '1.20MB'
        1253656678 => '1.17GB'
    """
    factor = 1024
    for unit in ["", "K", "M", "G", "T", "P"]:
        if bytes < factor:
            return f"{bytes:.2f}{unit}{suffix}"
        bytes /= factor

# Findet die Auflösung heraus
def get_screen_size():
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    return f"{screen_width}x{screen_height}"

def get_sys_uptime():

    # System-Startzeit ermitteln
    boot_time_timestamp = psutil.boot_time()
    boot_time = datetime.datetime.fromtimestamp(boot_time_timestamp)

    # Aktuelle Zeit
    now = datetime.datetime.now()

    # Uptime berechnen
    uptime = now - boot_time

    # Uptime in Stunden und Minuten umrechnen
    uptime_hours, remainder = divmod(uptime.total_seconds(), 3600)
    uptime_minutes = remainder // 60

    # Weist an das diese Funktion Stunden und Minuten ausgeben soll
    return f"{int(uptime_hours)} h , {int(uptime_minutes)} m"

# Setzt das korrekte Logo für die Distro
def get_distro_logo():
    if distro_id == "debian":
        distro_icon.configure(image=debian_logo)
    elif distro_id == "arch":
        distro_icon.configure(image=arch_logo)        
    elif distro_id == "mint":
        distro_icon.configure(image=mint_logo)
    elif distro_id == "ubuntu":
        distro_icon.configure(image=ubuntu_logo)
    elif distro_id == "opensuse":
        distro_icon.configure(image=osuse_logo)
    elif distro_id == "fedora":
        distro_icon.configure(image=fedora_logo)
    else:
        distro_icon.configure(image=distro_logo)

# Vars für die Labels
# Ließt den User aus
user = os.environ["USER"]
# Ließt den Host aus
hostname = socket.gethostname()
# Ließt den Pretty Name  aus
os_release_pretty = distro.name(pretty=True)
# Ließt den Kernel aus
kernel_release = platform.release()
# Basis um den RAM auszulesen
svmem = psutil.virtual_memory()
# Basis um CPU-Werte auszulesen
cpu_freq = psutil.cpu_freq()
# Ließt Anzahl der CPU-Kerne aus
cpu_core_count = psutil.cpu_count(logical=False)
# Gibt die aktuelle Shell aus
active_shell = os.environ["SHELL"]
# Gibt die Distro-ID aus
distro_id = distro.id()

# Erstelle das Hauptfenster
root = tk.Tk()
root.title("Neofetch-Tk")
root.geometry("800x500")
root["background"]="#FFFFFF" # Weiß

# Distro Logos
distro_logo = tk.PhotoImage(file="images/test.png")
arch_logo = tk.PhotoImage(file="images/arch_logo_350.png")
debian_logo = tk.PhotoImage(file="images/debian_logo_350.png")
mint_logo = tk.PhotoImage(file="images/mint_logo_350.png")
suse_logo = tk.PhotoImage(file="images/osuse_logo_350.png")
ubuntu_logo = tk.PhotoImage(file="images/ubuntu_logo_350.png")
fedora_logo = tk.PhotoImage(file="images/fedora_logo_350.png")

# Einen Frame Zeichen
logo_frame = tk.Frame(root,background="#FFFFFF")
logo_frame.pack(fill="both",expand=False,side='left',padx=10,pady=10)

# Distro-Logo-Label
distro_icon = tk.Label(logo_frame,text="DISTRO LOGO",image=distro_logo,background="#FFFFFF")
distro_icon.pack(anchor=tk.NW)

# Einen Frame Zeichen
stat_frame = tk.Frame(root,background="#FFFFFF")
stat_frame.pack(fill="both",expand=True,side='left',padx=10,pady=10)

# Label mit Text USER@HOST
user_host_label = tk.Label(stat_frame,text=f"{user}@{hostname}",background="#FFFFFF",font=("Sans",14))
user_host_label.pack(anchor=tk.NW)

# Label mit Text OS:
os_label = tk.Label(stat_frame,text=f"OS: {os_release_pretty}",background="#FFFFFF",font=("Sans",14))
os_label.pack(anchor=tk.NW)

# Label mit Text Host:
host_label = tk.Label(stat_frame,text=f"Host: {hostname}",background="#FFFFFF",font=("Sans",14))
host_label.pack(anchor=tk.NW)

# Label mit Text Kernel:
kernel_label = tk.Label(stat_frame,text=f"Kernel: {kernel_release}",background="#FFFFFF",font=("Sans",14))
kernel_label.pack(anchor=tk.NW)

# Label mit Text Uptime:
uptime_label = tk.Label(stat_frame,text=f"Uptime: {get_sys_uptime()}",background="#FFFFFF",font=("Sans",14))
uptime_label.pack(anchor=tk.NW)

# Label mit Text Shell:
shell_label = tk.Label(stat_frame,text=f"Shell: {active_shell}",background="#FFFFFF",font=("Sans",14))
shell_label.pack(anchor=tk.NW)

# Label mit Text Resolution:
res_label = tk.Label(stat_frame,text=f"Resolution: {get_screen_size()}",background="#FFFFFF",font=("Sans",14))
res_label.pack(anchor=tk.NW)

# Label mit Text DE:
de_label = tk.Label(stat_frame,text=f"DE: {get_desktop_environment()}",background="#FFFFFF",font=("Sans",14))
de_label.pack(anchor=tk.NW)

# Label mit Text Window-Manager:
wm_label = tk.Label(stat_frame,text=f"WM: {get_window_manager_name()}",background="#FFFFFF",font=("Sans",14))
wm_label.pack(anchor=tk.NW)

# Label mit Text CPU:
cpu_label = tk.Label(stat_frame,text=f"CPU: ({cpu_core_count}) @ {cpu_freq.max:.2f} Mhz",background="#FFFFFF",font=("Sans",14))
cpu_label.pack(anchor=tk.NW)

# Label mit Text Memory:
mem_label = tk.Label(stat_frame,text=f"Memory: {(get_size(svmem.used))}/{get_size(svmem.total)}",background="#FFFFFF",font=("Sans",14))
mem_label.pack(anchor=tk.NW)

# Führt get_distro_logo aus
get_distro_logo()

# Starte die Hauptschleife
root.mainloop()

Der ganze Code auf Github.

Link zu den vorangegangenen Teilen

Tags

Python, GUI, Neofetch, Tkinter

GSe
Geschrieben von GSe am 7. September 2024 um 07:33

Sehr schöne Reihe auch sehr gut erklärt, das einzige was mir so stört, das kein Link zu der ersten Teil hier zu finden ist. Oder vielleicht ein Art Listing, von den Python Tutorials, was schon erschienen ist. Denke das würden sich, natürlich ich auch, drüber freuen ;D

Actionschnitzel
Geschrieben von Actionschnitzel am 7. September 2024 um 14:50

Mein Fehler, wird im Laufe des Tages gefixed.

Micha
Geschrieben von Micha am 7. September 2024 um 14:32

Erst mal vielen Dank für dieses tolle Anleitungen. Ich hätte 2 Anmerkungen:

  • Bei allen Anzeigen steht vor dem "Wert" der Name nur bei User nicht? Ist dies Absicht? Ich habe bei mir "User: " hinzugefügt.
  • Bei den Labels habe ich ", foreground="#000000"" am Ende hinzugefügt, da ansonsten der anzuzeigende Text nicht sichtbar ist, die Textfarbe scheint hier identisch mit der Hintergrundfarbe zu sein.
Actionschnitzel
Geschrieben von Actionschnitzel am 7. September 2024 um 14:48

Hi, bei neofetch steht ja auch nicht "USER:". Ich hallte mich erstmal an den Aufbau von Neofetch und wenn alle Probleme ausgebügelt sind, kommen wir sowieso in die Phase, dass wir dem Programm einen eigenen Look geben. Das wird sich das dann auch ändern.

GSe
Geschrieben von GSe am 7. September 2024 um 16:26

Das hört sich super an ;D Finde so ein Tutorial besser, als ein Video. Einiges kann man vielleicht besser mit Video zeigen, aber solche Sachen finde ich gehören schritlich dargestellt ;D Weiss nicht, wieviel Arbeit ein Video macht, aber der Haken ist halt, das auch das drumherum stimmen muss, also heisst sound, der Ablauf etc. Einige sind dann zu leise, der andere hat einen Sound wie aus einem Telefon.

Daher nochmals ein gannnzzz grossses Danke an dich und auch an das Team von GNU/Linux.ch ;D

gze
Geschrieben von gze am 11. September 2024 um 19:10

Im Tag für Tkinter ist ein Typo drin. Wäre super, wenn man den korrigieren würde, dann wären die anderen Artikel aus der Serie auch einfacher zu finden.