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

  Actionschnitzel   Lesezeit: 15 Minuten Auf Mastodon ansehen

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

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

Frisch ans Werk. Zuletzt haben wir uns mit dem Styling beschäftigt. Was wir gelernt haben, war aber nur die Spitze des Eisbergs. Fürs Erste soll uns dieser Wissensstand aber reichen.

Heute soll es um das Auslesen des Window-Managers gehen, ich muss aber weit ausholen: Wir benötigen das Tool wmctrl aus unserem Repository.

Das Tool lässt sich über das Terminal mit bash ansteuern. Soweit so gut, wir lernen hier aber Python.

sudo apt install wmctrl
  -m                   Show information about the window manager and
                       about the environment.

Beispiel: Gnome

~$ wmctrl -m
Name: GNOME Shell
Class: N/A
PID: N/A
Window manager's "showing the desktop" mode: OFF

Hier besagt der Name, dass es sich um GNOME Shell handel. Wir wissen aber natürlich, dass der Window-Manager MUTTER heißt. Darum kümmern wir uns auch noch.

Beispiel: XFCE

~$ wmctrl -m
Name: Xfwm4
Class: xfwm4
PID: 2043
Window manager's "showing the desktop" mode: OFF

Dieses kleine Problem bringt uns zu unserer nächsten Übung.

System-Befehle in Python ausführen

Es gibt hier mehrerer Herangehensweisen, wobei die ersten beiden also veraltet gelten. Dennoch sind sie gut um ein Verständnis für die Funktionsweise zu erlangen.

Es handelt sich hier um Übungen, die so nicht im eigentlichen Tool vorkommen werden. Legt euch also jeweils Beispiel-Skripte an, z.B.: os_system_test.py

os.system

https://docs.python.org/3/library/os.html#os.system

import os

os.system("x-terminal-emulator")

Wenn alles gut geht, sollte sich der Default-Terminal-Emulator öffnen und wir haben somit das System angesteuert. os.system hat aber leider einen kleinen Nachteil. Es blockiert den Loop und erst, wenn der angesteuerte Prozess beendet ist, geht es weiter. Um das zu veranschaulichen, machen wir Folgendes:

import os

os.system("x-terminal-emulator")

print("Hallo")

os.system("x-terminal-emulator")

print("Hallo")

Ihr werdet merken, dass der Print-Befehl erst ausgegeben wird, wenn Ihr das Terminal-Fenster geschlossen habet. Natürlich können wir hiermit viel mehr anstellen, als nur ein Terminal zu öffnen. Im Grunde können wir innerhalb der Quotes genauso wie in der Bash agieren. 

import os

os.system("x-terminal-emulator -e pkexec apt update")

Die Flag `-e` weist hier an, dass in der `bash` des Terminal-Emulators etwas ausgeführt werden soll. Euch sollte auch aufgefallen, dass sich das Terminal-Fenster selbstständig geschlossen hat. Für eine GUI-Anwendung ist das relativ unschön, kann aber auch mit etwas Bash-Voodoo umgangen werden. Das lassen wir aber erstmal außen vor.

Oder:

import os

os.system("pkexec apt update")

Bei diesem Beispiel wird der Output direkt über die Python-Konsole abgewickelt.

Wer nicht unbedingt sein Repository auffrischen will, kann natürlich auch `ls` statt `apt update` nehmen.

os.popen

https://docs.python.org/3/library/os.html#os.popen

Diese Methode funktioniert ähnlich, ist aber flexibler.

import os

os.popen("x-terminal-emulator")

print("Hallo")

os.popen("x-terminal-emulator")

print("Hallo")

Wenn Ihr denn Code startet, solltet alle Aktionen sofort hintereinander ohne Interaktion ausgeführt werden.

import os
output = os.popen("ls").read()
print(f"Output:{output}")

Logik:

  • Die Variable `output` hält `os.popen("ls").read()`.
  • `.read()` least den Inhalt aus.
  • über `print` lässt sich der Inhalt der Variable auslesen.

subprocess.run

https://docs.python.org/3/library/subprocess.html#subprocess.run

Wesentlich flexibler, auskunftsfreudiger, aber auch komplizierter ist das Modul `subprocess.run`

import subprocess

result = subprocess.run(["ls"], capture_output=True, text=True)
print("Erfolg?:", result.returncode)
print("Ausgabe:", result.stdout)

Auf wmctrl bezogen bedeutet das für uns:

import subprocess

result = subprocess.run(["wmctrl","-m"], capture_output=True, text=True)
print("Erfolg?:", result.returncode)
print("Ausgabe:", result.stdout)

Ausgabe:

>>> %Run -c $EDITOR_CONTENT
Erfolg?: 0
Ausgabe: Name: GNOME Shell
Class: N/A
PID: N/A
Window manager's "showing the desktop" mode: OFF

Das schöne hierbei ist, dass mit .returncode können wir den Status auslesen. Wir haben hier eine nicht-interaktive Applikation, die im Grunde fehlerfrei laufen sollte. Nehmen wir mal an, wir wollen ein Update durchführen und es existiert keine Internetverbindung. Ist dem so würde der Fehler-Code 1 ausgegeben. Um dem Nutzer rückzumelden, dass etwas schiefgelaufen ist, könnten wir nun einbauen, dass bei der Ausgabe ein Pop-up-Fenster geöffnet werden soll, welches den Nutzer darauf hinweist.

Einbau in unseren Code

import subprocess

def get_window_manager_name():
    result = subprocess.run(["wmctrl", "-m"], capture_output=True, text=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

Logik:

  • ["wmctrl", "-m"]- Jedes Segment wird in Anführungszeichen gesetzt und durch Kommata getrennt.
  • Mit capture_output=True wird ausgesagt, dass der Output erfasst werden soll.
  • text=True legt fest, dass der Output als String ausgegeben wird.
  • Über .stdout wird sich der Output geholt.
  • strip() entfernt führende und nachfolgende Leerzeichen.
  • Durch split("\n") werden Absätze zu Listen umgewandelt.
  • for line in output_lines: Für jetze ausgelesene Zeile.
  • if line.startswith("Name: ") prüft, ob eine Zeile mit "Name:" beginnt.
  • Mit line.split("Name: ")[1] wird die Zeile nach "Name: " geteilt.
  • Wenn der Output "GNOME Shell" soll MUTTER ausgegeben werden.
  • Ansonsten wird das Ergebnis normal zurückgegeben.

Der ganze Code

## Teil 10 ###

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

# 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 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()

Wie angedroht wird es komplizierter... Bis nächste Woche ;-)

Alles auf Github

Link zu den vorangegangenen Teilen

Tags

Python, Tkinter, GUI, Neofetch

Es wurden noch keine Kommentare verfasst, sei der erste!