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

  Actionschnitzel   Lesezeit: 16 Minuten  🗪 4 Kommentare Auf Mastodon ansehen

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

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

Alles, was wir bis Teil 4 gemacht haben, war eigentlich nur ein Bild zu zeichnen. Das ist nicht nur eine Metapher, sondern durchaus korrekt dargestellt. Wir haben Code geschrieben, der dem Fenster-Manager sagt: Ich brauche ein Fenster mit zwei farbigen Rahmen und einem Bild darin. Wirklich etwas falsch machen kann man hier nicht, außer fehlerhaften Code einzugeben. Der Compiler würde uns dann schon sagen, was nicht läuft. Wir können das bisher Erarbeitete also als Kunst verbuchen, und Kunst ist nun mal Ansichtssache.

Nun sind wir fürs Erste fertig mit diesem Teil und wollen dazu übergehen, Informationen auszulesen und sie unserem GUI anzeigen zu lassen. Ab jetzt gibt es unzählige Möglichkeiten und Varianten, dies zu tun. Wir kommen in einen Bereich, der viel Lektüre beansprucht und bei dem es um Lesbarkeit, Simplizismus, Effektivität und auch Sicherheit geht.

Es wird unvermeidbar sein, auf Bugs zu stoßen oder Probleme mit Distro-übergreifenden Funktionen zu bekommen. Sprich: Wir werden die einfachsten Lösungen nutzen, wir werden Fehler machen und versuchen, diese zu beseitigen, aber wir müssen uns auch damit auseinandersetzen, dass das, was wir zusammen gebaut haben, vielleicht gar nicht auf den Systemen anderer Leute funktioniert.

Unser Workflow:

DENKEN > Code schreiben > Testen > DENKEN > Fehler beheben > Testen

Der User

Es gibt dutzende Wege, den aktuellen Nutzer auszulesen. Wir gehen den Weg über das os-Modul. Hiermit kann man so einiges nicht nur auslesen, sondern auch manipulieren. Im Laufe der Serie werden wir `os` noch besser beleuchten; die schiere Menge an Möglichkeiten kann aber schnell überfordern. Wie immer gilt: Eines nach dem anderen.

Das Modul ist standardmäßig in Python implementiert, und ihr müsst nichts nachinstallieren.

os.getlogin()

Return the name of the user logged in on the controlling terminal of the process. For most purposes, it is more useful to use getpass.getuser() since the latter checks the environment variables LOGNAME or USERNAME to find out who the user is, and falls back to pwd.getpwuid(os.getuid())[0] to get the login name of the current real user id.

python.org

Test-Code

import os

user = os.getlogin()

print(user)

Der Output sollte Euer Nutzer-Name sein.

Einbauen

Zu unserer Importliste fügen wir import os. Nun legen wir die Variable user = os.getlogin()an. Als Nächstes müssen wir den Text des Labels manipulieren. Jetzt kommt der f-String ins Spiel.

Vorher:

user_host_label = tk.Label(stat_frame,text="USER@HOST")
user_host_label.pack(anchor=tk.NW)

Nachher:

user_host_label = tk.Label(stat_frame,text=f"{user}@HOST")
user_host_label.pack(anchor=tk.NW)

Logik

  • Vor die Quotes(" ") wird ein "f" gesetzt, um den String zu manipulieren
  • Es wird USER ersetzt durch user in { } (geschweifte Klammern)
  • die Variable user am Anfang des Codes wird gespeichert und innerhalb des Strings ausgegeben

Der Host

Diesen können wir mit dem socket-Modul auslesen, welches ebenfalls in Python enthalten sein sollte. Es bietet Zugriff auf die Netzwerk-Schnittstellen des Betriebssystems und ermöglicht es, Netzwerkkommunikation mittels Sockets durchzuführen. Ein Socket ist ein Endpunkt einer bidirektionalen Kommunikationsverbindung zwischen zwei Programmen, die über ein Netzwerk kommunizieren. 

socket-Modul

Wir benötigen erstmal nur eine Funktion dieses Moduls

socket.gethostname()

Einbauen

Wir auch zuvor binden wir socket in den Code ein mit import socket, Dann legen wir die nötige Variable an. 

hostname = socket.gethostname()

Ich hoffe, ihr hab aufgepasst und wisst, was nun folgt. Die Erklärung solltet ihr Euch selbst erschließen können.

user_host_label = tk.Label(stat_frame,text=f"{user}@{hostname}")
user_host_label.pack(anchor=tk.NW)

Der Code bis hier

## Teil 5 ###
import tkinter as tk
from PIL import Image, ImageTk
import os
import socket

user = os.getlogin()
hostname = socket.gethostname()

# Erstelle das Hauptfenster
root = tk.Tk()
root.title("Neofetch-Tk")
root.geometry("800x500")

distro_logo = tk.PhotoImage(file="images/test.png")

# Einen Frame Zeichen
logo_frame = tk.Frame(root,background="yellow")
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="yellow")
distro_icon.pack(anchor=tk.NW)

# Einen Frame Zeichen
stat_frame = tk.Frame(root,background="cyan")
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}")
user_host_label.pack(anchor=tk.NW)

# Starte die Hauptschleife
root.mainloop()

Ist Euch eigentlich etwas aufgefallen? Nein? Schade. In Teil 4 war 2x user_host_label vorhanden. Python liest das File von oben nach unten durch und es ist dem Interpreter egal, ob die vorherige Variable genauso hieß. Bei Labels ist das weiter nicht schlimm. Wenn ihr aber eine Variable mit einem wichtigen, wer verseht z.B. zahl_eins = 1 und danach aus Versehen zahl_eins = 2 als setzt, ist kann das zum Problem werden. Also achtet auf die Benennung.

OS Label

Wir korrigieren nun diesen Fehler und benennen die Variable um, das sollte dann so aussehen:

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

# Label mit Text OS:
os_label = tk.Label(stat_frame,text=f"OS: ")
os_label.pack(anchor=tk.NW)

Wie kommen wir jetzt an unseren OS-NAMEN?

Wir werfen zunächst einen Blick in /bin/neofetch. Ob Ihr das nun mit cat macht, oder Eurem Liebling-Editor ist vollkommen egal. Um die betreffende Stelle zu finden, müssten wir zunächst an hunderten auskommentierte Zeilen vorbei.

Nicht ganz ausführlich, aber ausreichten erklärt holt sich neofetch die Information aus /etc/os-release und/oder /etc/lsb-release. Das ist noch wichtig zu wissen für später.  

Code-Block auf Github

Wie wir das nachbilden können, habe ich schon in dem Artikel zur Terminal-Version veranschaulichen:

# Get Name of Distro
os_release = subprocess.run(["grep", "-E", "^(PRETTY_NAME)=", "/etc/os-release"], stdout=subprocess.PIPE, text=True)
nice_name = os_release.stdout.strip().split('=')[1].strip('"')
print(f"OS: {nice_name}")

Das ist aber viel zu kompliziert und dieses Konzept der Daten-Erfassung werden wir uns zu einem späteren Zeitpunkt anschauen.

Es gibt ein Python-Modul, das es relativ einfach macht, den os-release auszulesen.

distro

Sollte auf Eurem Debian/Ubuntu-Derivat das Paket nicht installiert sein:

sudo apt install python3-distro

Wir wollen also den Pretty-Name und den bekommen wir so:

import distro

pretty_name = distro.name(pretty=True)
print("Pretty Name:", pretty_name)

>>>  Pretty Name: Ubuntu 24.04 LTS (In meinem Fall)

Logik

  • distro ist das Modul
  • name die Methode
  • mit distro.name greifen wir darauf zu.
  • distro.name(pretty=True)

Ohne pretty=True würde das so aussehen:

import distro

pretty_name = distro.name()
print("Pretty Name:", pretty_name)

>>> Pretty Name: Ubuntu

Ganz ausführlich wäre:

import distro

print(f"Pretty Name: {distro.name(pretty=True)}")
print(f"Name: {distro.name()}")
print(f"Version: {distro.version()}")
print(f"ID: {distro.id()}")
print(f"Alles: {distro.info()}")

Distro auf PyPI

Das Modul in unseren Code einbauen

Ihr bekommt jetzt eine kleine Denkaufgabe:

  • distro importieren
  • die Variable os_release_pretty setzen
  • os_release_pretty in den Text von os_label einbauen

Der Ganze Code

## Teil 5 ###
import tkinter as tk
from PIL import Image, ImageTk
import os
import socket
import distro

user = os.getlogin()
hostname = socket.gethostname()
os_release_pretty = distro.name(pretty=True)

# Erstelle das Hauptfenster
root = tk.Tk()
root.title("Neofetch-Tk")
root.geometry("800x500")

distro_logo = tk.PhotoImage(file="images/test.png")

# Einen Frame Zeichen
logo_frame = tk.Frame(root,background="yellow")
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="yellow")
distro_icon.pack(anchor=tk.NW)

# Einen Frame Zeichen
stat_frame = tk.Frame(root,background="cyan")
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}")
user_host_label.pack(anchor=tk.NW)

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

# Starte die Hauptschleife
root.mainloop()

Zum Abschluss

Ich habe schon ein paar Mal darauf hingewiesen, dass wir ab jetzt in Bug-Territorium kommen. Vorweg nehme ich schon einmal, dass es sein kann, dass z.B. auf MX-Linux in der OS-Zeile "GNU/Linux Debian 12" stehen könnte. Das ist aber nicht unser Fehler, sondern der Fehler der Distro-Entwickler. Es gibt die eine oder andere Distribution, die in os-release nicht ausreichend deklariert, wie denn nun deren Derivat heißt. Letztendlich helfen Schuldzuweisungen auch nicht weiter. Wir bekommen das aber hin.

Um so etwas zu umgehen, müssten wir noch zusätzliche Informationen einbinden. Ich gehe auch nur kurz darauf ein. Sagen wir, die Distro gibt den Wert Debian aus, wir aber auf MX unterwegs sind, können wir es etwas zurechtbasteln.

Beispiel: Wenn die Distro Debian ergibt und die Datei /bin/mx-tools existiert, soll os_release_pretty den Wert MX-Linux ausgeben. Das ist nicht unbedingt akkurat, denn wir wissen noch immer nicht, welche Versionsnummer und welchen Codenamen wir nutzen. Lass das erstmal sacken, mit diesen Mechaniken werden wir uns bald auseinandersetzen müssen.

Hausaufgabe

Sollte der OS-Output auf Eurem PC nicht Eurer Distro entsprechen, dann postet es doch mal unter dem Artikel. So können wir uns einen Überblick darüber machen, für welche Distros wir einen Workaround basteln müssen. Vielleicht findet sich ja in lsb_release etwas Brauchbares.

cat /etc/os-release
cat /etc/lsb-release

To-Do (aus Rückmeldungen)

Distro-Sonder-Fälle

  • Siduction: /etc/siduction-version

Get User Error

import os
user = os.environ["USER"]

# ODER:

import pwd
import os
user = pwd.getpwuid(os.getuid()).pw_name

Bis nächste Woche

Tags

Python, Tkinter, Neofetch, GUI, Coding

Sid
Geschrieben von Sid am 27. Juni 2024 um 22:52

Danke, ich freue mich auf die Fortsetzungen. Ich verwende die Distribution Siduction www.siduction.org.

lsb_release -a

Distributor ID: Debian Description: Debian GNU/Linux trixie/sid Release: n/a Codename: trixie

ls /etc/*version

-rw-r--r-- 1 root root 11 2023-06-11 12:00 /etc/debian_version -r--r--r-- 1 root root 49 2023-09-09 21:08 /etc/siduction-version

cat /etc/*version

trixie/sid siduction 2023.1.1 giants - xfce - (202309091902)

Actionschnitzel
Geschrieben von Actionschnitzel am 28. Juni 2024 um 02:14

Super, danke! Ich habe auch nochmal den Neofetch-Code nachgelesen. Für Siduction gibt es einen speziellen Fall, der dazu führt, dass /etc/siduction-version ausgelesen wird.

Naja
Geschrieben von Naja am 28. Juni 2024 um 09:26

Erst mal sorry für die tausend Zeilenumbrüche, aber die Syntax hier in dieser Feedback-Box ist mir nicht klar und es gibt keine Voransicht...

'user = os.getlogin()'

Bringt unter Ubuntu 22.04: OSError: [Errno 6] No such device or address

Nach kurzer Recherche zeigt sich, dass Python über die glibc eine /proc/self/loginuid ausliest, die wohl nicht überall gesetzt ist (?).

Auf diesem Rechner funktioniert es, entweder die Umgebungsvariable "LOGNAME" auszulesen:

os.environ["LOGNAME"]

Oder noch das Password-Modul zu laden und die passwd-Info auszulesen (das scheint mir unter Linux die sicherste Variante):

import pwd

pwd.getpwuid(os.geteuid())[0])

Actionschnitzel
Geschrieben von Actionschnitzel am 28. Juni 2024 um 09:50

Genau so etwas habe ich erwartet; ich habe ja schon oft erwähnt, dass es mit zunehmender Komplexität zu Fehlern kommen kann. Kommt auf die Liste. Entweder gehen wir mit:

import os user = os.environ["USER"]

ODER:

import pwd import os user = pwd.getpwuid(os.getuid()).pw_name

... Wie gesagt: Duzenden Möglichkeiten :-D