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

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

Heute werden wir „fertig“. Wenn überhaupt, steht am Ende dieses Artikels eine Alpha-Version. Dies ist der 15. Teil der Reihe, und wer es bis hierhin geschafft hat, kann stolz auf sich sein, denn leicht war es sicher nicht.

Wer diesen Artikel durchgearbeitet hat, sollte so etwas vor sich haben:

Wie geht es jetzt weiter?

Zunächst einmal werde ich mir die Kommentare unter allen Artikeln zu Gemüte führen. Ihr wart fleißig und habt viele Verbesserungsvorschläge und Anmerkungen eingebracht, die ebenfalls umgesetzt werden sollen.

Pakete zählen

Soweit ich weiß, ist es kein Verbrechen bei sich selbst zu klauen. Im Mai habe ich einen kleinen Artikel darüber geschrieben, wie man mithilfe von Python eine eigene Terminal-Version von Neofetch schreibt. 

Wir bedienen uns heute daran, denn in dem dort vorgestellten Code befindet sich etwas, das wir benötigen.

Gleich vorab sei gesagt, dass ich auf Debian programmiere. Wer es bis Teil 15 geschafft hat, dem traue ich zu, ein paar Modifikationen selbst vornehmen zu können. In klarer Sprache: Ich habe keine Ahnung, wie man unter anderen Paket-Managern eine Listung vornimmt, wie diese Leistungen strukturiert sind und wie man sie ausließt. Das ist eure kleine "Knobel-Aufgabe".

import subprocess # Get installed Debian Packages deb_count = subprocess.run(["dpkg", "--list"], stdout=subprocess.PIPE, text=True) deb_counted = len(deb_count.stdout.splitlines()) - 5 print(f"Packages: {deb_counted} (Debian)") >>> %Run -c $EDITOR_CONTENT Packages: 2772 (Debian)

Logik:

  • Der Subprocess liest über dpkg --list die Pakete aus, das nimmt nicht viel Zeit in Anspruch. Der Output sieht in gekürzter Form so aus:
Gewünscht=Unbekannt/Installieren/R=Entfernen/P=Vollständig Löschen/Halten | Status=Nicht/Installiert/Config/U=Entpackt/halb konFiguriert/ Halb installiert/Trigger erWartet/Trigger anhängig |/ Fehler?=(kein)/R=Neuinstallation notwendig (Status, Fehler: GROSS=schlecht) ||/ Name Version > +++-==============================================-============================> ii 7zip 23.01+dfsg-11 > ii aardvark-dns 1.4.0-5 > ii accountsservice 23.13.9-2ubuntu6 > ii acl 2.3.2-1build1.1 > ii adduser 3.137ubuntu1 > ii adwaita-icon-theme 46.0-1 [...]
  • Wir nutzen len, um ganz plump, die ausgegebenen Reihen zu zählen.
  • stdout macht das ganze für uns besser nutzbar
  • Mit splitlines()erhalten wir eine ordentliche Liste mit Zeilen
  • - 5 (minus fünf) rationalisiert die in der oberen Ausgabe zu sehende Header-Zeilen weg, welche keine Information über ein Paket beinhalten.

Einbau ein unser GUI

Im Grunde ändern wir nicht viel. Das ganze landet in einer Funktion, damit wir den Code weiter benutzen können. Statt print wollen wir, dass die Funktion den ermittelten Wert zurückgibt.

def get_pkg_count(): # Get installed Debian Packages deb_count = subprocess.run(["dpkg", "--list"], stdout=subprocess.PIPE, text=True) deb_counted = len(deb_count.stdout.splitlines()) - 5 return deb_counted

Dazu noch das obligatorische Label:

# Label mit Text Pakages: pkgs_label = tk.Label(stat_frame,text=f"Packages: {get_pkg_count()} (Debian)",background="#FFFFFF",font=("Sans",14)) pkgs_label.pack(anchor=tk.NW)

Der Terminal-Emulator

Ich hatte mich eigentlich darauf verlassen, dass jemand, der mehr Hirnschmalz besitzt als ich, schon irgendwo im Netz eine Methode bereitgestellt hat, um das "Terminal" auszulesen. Leider ist das nicht so.

Meine Hoffnung war, dass man über update-alternatives etwas auslesen kann. Nun ist es aber so – zumindest glaube ich –, dass openSUSE update-alternatives gar nicht nutzt. Wie es bei anderen Betriebssystemen aussieht, kann ich auch nicht genau sagen. Deshalb hier gleich die Vorwarnung, dass der Code nicht absturzsicher ist.

def get_terminal_emu(): result = subprocess.run( ["update-alternatives", "--query", "x-terminal-emulator"], stdout=subprocess.PIPE, text=True, check=True ) # Den relevanten Teil filtern for line in result.stdout.splitlines(): if line.startswith("Value:"): return line.split("bin/")[1].strip()

Logik

  • Wir holen uns über den Standard-Output den Wert von Value.
  • Dazu lesen wir jede Zeile mit einem For-Loop aus.
  • Und wenn eine Zeile Value enthält, soll diese Zeile angezeigt werden.
  • Über split schneiden wir den Pfad ab, sodass wir nur noch den Dateinamen haben.

Das ist keine saubere Methode, denn rein theoretisch könnte in der Ausgabe noch ein "Value" vorhanden sein. Sollte es bei euch nicht funktionieren, lasst die Ausgabe des Terminals vorerst weg oder versucht euch an eine eigene Lösung. 

Der ganze Code

## Teil 14 ### import tkinter as tk from PIL import Image, ImageTk import os import socket import distro import platform import psutil import datetime import subprocess from os import popen # Ließt den Desktop aus def get_desktop_environment(): xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP","").lower() if xdg_current_desktop == "x-cinnamon" or xdg_current_desktop == "cinnamon": return "CINNAMON" elif xdg_current_desktop == "unity": return "UNITY" elif xdg_current_desktop == "ubuntu:gnome": return "GNOME" elif "gnome" in xdg_current_desktop: return "GNOME" elif "plasma" == xdg_current_desktop or "kde" == xdg_current_desktop: return "KDE" elif "xfce" == xdg_current_desktop: return "XFCE" elif os.environ.get("DESKTOP_SESSION", "").lower() == "lxde-pi-wayfire": return "PI-WAYFIRE" elif "mate" == xdg_current_desktop: return "MATE" else: return "Unknown" # Ließt das KDE Theme aus def get_kde_theme_new(): result = subprocess.run(['plasmashell', '--version'],capture_output=True,text=True, check=True ) plasma_version = result.stdout.strip() if "plasmashell 5." in plasma_version: result = subprocess.run(['kreadconfig5', '--file', '~/.config/kdeglobals', '--group', 'KDE', '--key', 'LookAndFeelPackage'],stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) output = result.stdout.strip() return output if "plasmashell 6." in plasma_version: result = subprocess.run(['kreadconfig6', '--file', '~/.config/kdeglobals', '--group', 'KDE', '--key', 'LookAndFeelPackage'],stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) output = result.stdout.strip() return output # Ließt die KDE-Icons aus def get_kde_icons(): result = subprocess.run(['plasmashell', '--version'],capture_output=True,text=True, check=True ) plasma_version = result.stdout.strip() if "plasmashell 5." in plasma_version: result = subprocess.run(['kreadconfig5', '--file', '~/.config/kdeglobals', '--group', 'Icons', '--key', 'Theme'],stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) output = result.stdout.strip() return output if "plasmashell 6." in plasma_version: result = subprocess.run(['kreadconfig6', '--file', '~/.config/kdeglobals', '--group', 'Icons', '--key', 'Theme'],stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) output = result.stdout.strip() return output #print(get_kde_icons()) # Ließt das DE-Theme aus def get_desktop_theme(): if get_desktop_environment() == "GNOME": result = subprocess.run(['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "CINNAMON": result = subprocess.run(['gsettings', 'get', 'org.cinnamon.desktop.interface', 'gtk-theme'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "MATE": result = subprocess.run(['gsettings', 'get', 'org.mate.interface', 'gtk-theme'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "XFCE": result = subprocess.run(['xfconf-query', '-c', 'xsettings', '-p','/Net/ThemeName'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "PI-WAYFIRE": result = subprocess.run(['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "KDE": return get_kde_theme_new() def get_desktop_icons(): if get_desktop_environment() == "GNOME": result = subprocess.run(['gsettings', 'get', 'org.gnome.desktop.interface', 'icon-theme'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "CINNAMON": result = subprocess.run(['gsettings', 'get', 'org.cinnamon.desktop.interface', 'icon-theme'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "MATE": result = subprocess.run(['gsettings', 'get', 'org.mate.interface', 'icon-theme'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "XFCE": result = subprocess.run(['xfconf-query', '-c', 'xsettings', '-p'"/Net/IconThemeName","-s",],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "PI-WAYFIRE": result = subprocess.run(['gsettings', 'get', 'org.gnome.desktop.interface', 'icon-theme'],capture_output=True,text=True, check=True ) return result.stdout.strip().strip("'") if get_desktop_environment() == "KDE": return get_kde_icons() # 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) def get_pkg_count(): # Get installed Debian Packages deb_count = subprocess.run(["dpkg", "--list"], stdout=subprocess.PIPE, text=True) deb_counted = len(deb_count.stdout.splitlines()) - 5 return deb_counted def get_terminal_emu(): result = subprocess.run( ["update-alternatives", "--query", "x-terminal-emulator"], stdout=subprocess.PIPE, text=True, check=True ) # Den relevanten Teil filtern for line in result.stdout.splitlines(): if line.startswith("Value:"): return line.split("bin/")[1].strip() # 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 Pakages: pkgs_label = tk.Label(stat_frame,text=f"Packages: {get_pkg_count()} (Debian)",background="#FFFFFF",font=("Sans",14)) pkgs_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 Theme wm_theme_label = tk.Label(stat_frame,text=f"Theme: {get_desktop_theme()}",background="#FFFFFF",font=("Sans",14)) wm_theme_label.pack(anchor=tk.NW) # Label mit Text Icons icon_theme_label = tk.Label(stat_frame,text=f"Icons: {get_desktop_icons()}",background="#FFFFFF",font=("Sans",14)) icon_theme_label.pack(anchor=tk.NW) # Label mit Text Terminal terminal_label = tk.Label(stat_frame,text=f"Terminal: {get_terminal_emu()}",background="#FFFFFF",font=("Sans",14)) terminal_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


GNU/Linux.ch ist ein Community-Projekt. Bei uns kannst du nicht nur mitlesen, sondern auch selbst aktiv werden. Wir freuen uns, wenn du mit uns über die Artikel in unseren Chat-Gruppen oder im Fediverse diskutierst. Auch du selbst kannst Autor werden. Reiche uns deinen Artikelvorschlag über das Formular auf unserer Webseite ein.