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

  Actionschnitzel   Lesezeit: 22 Minuten  🗪 5 Kommentare Auf Mastodon ansehen

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

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

Zuletzt ging es um das Desktop-Environment. Für diesen und den nächsten Teil war es essenziell, das "DE" auszulesen, denn nur so haben wir eine Grundlage, um Informationen zu Thema und Icons zu erlangen.

Die Logik dahinter  

Wir haben bereits gelegentlich mit if/else-Statements gearbeitet, und auch in diesem Teil wird das der Fall sein. Wir wissen also, welcher Desktop aktuell auf dem System läuft. Das ist gut, denn nun können wir die Möglichkeiten eingrenzen.

Das ist unser Code vom letzten Mal. Bei mir gibt er den Wert GNOME aus.

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())
>>> %Run -c $EDITOR_CONTENT
GNOME

Diese Funktion oder vielmehr die Ausgabe können wir nicht nur nutzen, um sie in einem Label anzeigen zu lassen. Wir können wie auch weiter verarbeiten.

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

if get_desktop_environment() == "GNOME":
    print("True")
>>> %Run -c $EDITOR_CONTENT
GNOME
True

Logik

  • Wenn der Desktop dem hart-kodierten String entspricht, soll "Wahr" ausgegeben werden.

Ein bisschen professioneller wäre es den Boolean, also den True- oder False-Wert auszulesen.

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

def is_gnome():
    return get_desktop_environment() == "CINNAMON"

print(is_gnome())
>>> %Run -c $EDITOR_CONTENT
False

Mehr zu boolean

Wo liegen die Informationen über das aktuelle Theme?  

Der Code zuvor war noch der einfache Teil. Wie bereits im letzten Abschnitt erwähnt, hat jede Desktop-Oberfläche ihre eigene Art, Daten zu verwalten. Was es uns etwas einfacher macht, ist, dass einige der beliebtesten Desktops Forks bzw. Abkömmlinge von GNOME sind. Somit können wir bereits ein gewisses Schema ableiten.

Für GNOME eignet sich gsettings gut, um das Theme auszulesen. Darüber habe ich schon vor einiger Zeit einen Artikel geschrieben, den ihr euch für ein besseres Verständnis durchlesen solltet.

Wie holt sich Neofetch das Theme?

Erste Anhaltspunkte, wie Neofetch das Theme erkennt, finden wir in Zeile 3216. Auf den ersten Blick bringt uns der Code nicht viel, aber bei genauerem Hinsehen wird es erhellend. Was wir hier haben, sind Variablen, die nach Tools benannt sind, die zur Konfiguration des Desktops verwendet werden. Diese Variablen halten den auszulesenden Wert als String – in diesem Fall das Theme.

Wenn wir uns die letzte Zeile ansehen, finden wir get_style. Dies verweist auf eine weitere Funktion und sollte genauer untersucht werden.

get_theme() {
    name="gtk-theme-name"
    gsettings="gtk-theme"
    gconf="gtk_theme"
    xfconf="/Net/ThemeName"
    kde="Name"

    get_style

}

Und Jackpot! Da ich nur ins Blaue geraten und nach dem Begriff "Theme" gesucht habe, ist mir entgangen, dass get_style direkt darüber liegt. Ab Zeile 3124 finden wir alle Kommandos, die Neofetch nutzt, um das Desktop-Theme auszulesen. Ihr könnt gerne noch etwas in den verlinkten Zeilen stöbern, aber ich liste uns schon mal unsere Optionen auf.

Cinnamon

gsettings get org.cinnamon.desktop.interface "$gsettings"

Gnome | Unity | Budgie

gsettings get org.gnome.desktop.interface "$gsettings"

Mate

gsettings get org.mate.interface "$gsettings"

XFCE

xfconf-query -c xsettings -p "$xfconf"

Test

Wir nutzen hier wieder subprocess.run welches wir schon kennengelernt haben. Zu beachten ist, dass dieser Code nicht fehlerfrei bzw. crash-sicher ist. Das rührt daher, dass versucht wird ein GTK-Theme auszulesen und ja nun mal nicht jeder Gnome nutzt. Weiter oben habe ich schon eine Liste aufgeführt, wie man die Themes verschiedener Desktops ausliest. Versucht doch, als kleine Übung den Code so zu modifizieren, dass er für eueren Desktop passt.

import subprocess

def get_desktop_theme_test():
    result = subprocess.run(
        ['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'], 
        stdout=subprocess.PIPE, 
        stderr=subprocess.PIPE,
        text=True
    )
    return result.stdout.strip().strip("'")

#get_desktop_theme_test()
print(f"{get_desktop_theme_test()}")

Warnung: Heavy Testing

Code zu schreiben und theoretisch wissen, wie es geht, ist eine Sache. Ob dann wirklich alles so läuft, ist wieder etwas ganz anderes. Einen Schwung SSDs mit verschiedenen Betriebssystemen zu haben ist übrigens Gold wert, wenn man übergreifend arbeitet.

Beim Testen ist mir aufgefallen, das XDG_CURRENT_DESKTOP auf Raspberry Pi OS None ausgibt. Dadurch kann somit auch nicht das Theme ausgelesen werden. Wir müssen mit dem Wert aus DESKTOP_SESSION gehen.

Hier eine verbesserte und er weitete Version von get_desktop_enviornment()

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"

Logik

  • Da dieser Suchlauf auf Groß- und Kleinschreibung achtet, nutze ich, ⁣ damit.lower() die Ausgabe komplett kleingeschrieben ist.
  • Sollte nichts zutreffen, wird zusätzlich noch DESKTOP_SESSION abgefragt

Anmerkung: Es geht natürlich eleganter, aber um zu verstehen, wie die Abfrage funktioniert, ist das die übersichtlichste Formulierung. Um die Ausgabe und bzw. Groß- und Kleinschreibung kümmern wir uns noch.

Theme Funktion

Sortiert nach Desktops, werden die Daten ausgelesen und über return zurückgegeben. Zu beachten ist, dass in diesem Beispiel KDE None zurückgibt. 

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 "None"

Logik

  • Wenn der Desktop dem hart-kodierten String (z.B. CINNAMON) entspricht, wird die eingereihte Aktion ausgeführt.
  • .strip(" ' ") entfernt die Anführungszeichen, die andernfalls mit ausgegeben würden.

Mehr zu strip

Spezial-Fall KDE

Die Desktop-Umgebung hat mir viel Kopfzerbrechen bereitet. Als Anhaltpunkt habe ich zwar den Code von Neofetch, reibungslos geht aber anders.

Wir haben zwar in Zeile 3137 den Verweis auf die ~/.config/kdeglobals, bei meinem Test auf Kubuntu wurde dieses File aber erst angelegt, als ich das Theme zum ersten Mal geändert habe. Darauf muss man auch erstmal kommen.

Leider habe ich auch nach langem Suchen keine so richtig gute und erprobte Methode gefunden, um das Theme auszulesen.

Selbst-Testen ist also das Gebot der Stunde. Bei geöffnetem ~/.config/kdeglobals habe ich das Theme geändert, daraus hat sich ergeben, dass sich der Parameter LookAndFeelPackage verlässlich auslesen lässt und das globale Theme wieder gibt.

Wir gehen also mit diesem Parameter und müssen diesen in einer eigenen Funktion auslesen, damit es auch übersichtlich bleibt.

def get_kde_theme():
    file_path = os.path.expanduser("~/.config/kdeglobals")

    with open(file_path, 'r') as file:
        for line in file:
            if line.startswith("LookAndFeelPackage="):
                look_and_feel = line.strip().split("=")[-1]

                if look_and_feel.startswith("org.kde."):
                    look_and_feel = look_and_feel.replace("org.kde.", "")
                return look_and_feel
  • file_path = os.path.expanduser("~/.config/kdeglobals")definiert den Pfad zur Datei. expanduser tauscht ~ gehen den tatsächlichen Pfad aus (⁣~ kann manchmal zu Verhakungen / Fehlern führen)
  • with open(file_path, 'r') as file: gibt die Anweisung, dass nun das File in der Variable geöffnet werden soll. 'r' legt fest, dass nur gelesen werden darf. Um damit weiter zu arbeiten wir mit as file alles in eine Variable verpackt.
  •    for line in file: Dieser For-Loop durchsucht jede Zeile der Datei. line kann aber auch i oder x genannt werden, das ist nicht festgelegt.
  • if line.startswith("LookAndFeelPackage="): Sollte eine Zeile den quotierten Begriff enthalten, wird die gesamte Zeile als Variable look_and_feel gleichgesetzt, mit strip auseinander gezogen und mit nur das einbezogen, was nach "=" steht. Mit [-1] wird sichergestellt das kein Absatz mitgenommen wird

Mehr zu open

Ergebnisse

Wie man gut sehen kann, wird das Theme korrekt ausgelesen. Lasst euch nicht von den falschen Distro-Logos oder Namen stören, das gehört alles noch zum Plan. 

Kubuntu

MX Linux

Raspberry Pi OS

Der ganze Code

## Teil 12 ###

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 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():
    file_path = os.path.expanduser("~/.config/kdeglobals")

    with open(file_path, 'r') as file:
        for line in file:
            if line.startswith("LookAndFeelPackage="):
                look_and_feel = line.strip().split("=")[-1]

                if look_and_feel.startswith("org.kde."):
                    look_and_feel = look_and_feel.replace("org.kde.", "")
                return look_and_feel

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

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

Christoph
Geschrieben von Christoph am 27. September 2024 um 20:23

Das ist jetzt alles richtig neu und interessant. Vielen Dank! Zwei Dinge wollte ich noch fragen, die hier vielleicht in den Rahmen passen könnten:

  1. Der Code ist ja auf Github. Ich verwende zwar hin und wieder git auf meinem lokalen Rechner, aber vor Github (und anderen) habe ich ziemlich Respekt. Klar, es gibt eine Einführung. Aber ich wäre für ein paar Handreichungenk, Kommentare dazu und Praxistipps sehr dankbar.
  2. Für tkinter gab und gibt es eine Reihe von Extensionen und Modifikationen. Eine rezente, die mir als gut aussehend aufgefallen ist, ist CustomTkinter. Was wären da die Pros und Cons?
Actionschnitzel
Geschrieben von Actionschnitzel am 27. September 2024 um 23:22

GitHub ist schon fast ein Fass ohne Boden. Ich habe hier ein Buch von Öggler und Kofler, das mir zur Seite steht.

Was CustomTkinter angeht:

Ja, das sieht super aus. Damit habe ich natürlich auch schon herumgespielt. Für Tkinter-Anfänger würde ich allerdings empfehlen, zuerst Tkinter und ttk zu lernen (als Grundlage). CT (CustomTkinter) baut zwar auf der Syntax von Tkinter auf, bringt aber viele Änderungen mit, die erst erlernt werden müssen.

Mein letzter Stand ist, dass man CT nur über PIP installieren kann. Auf Distributionen mit Debian-Basis kann man externe Pakete ja nur noch in einer virtuellen Python-Umgebung installieren. Ich versuche hier erstmal nur auf Pakete aus dem Repo zurückzugreifen.

MichaelM
Geschrieben von MichaelM am 28. September 2024 um 18:58

Zwei Hinweise als KDE-User:

  • Bei mir steht beim Theme am Ende noch ein .desktop, was man evtl. auch abschneiden müsste, wenn es einen stört
  • wenn man sich den Split mit "=" sparen möchte, es gibt mit "kreadconfig6 --file ~/.config/kdeglobals --group KDE --key LookAndFeelPackage" (bzw. bei Plasma 5 = kreadconfig5) die Möglichkeit, den Wert direkt zu lesen (mit subprocess analog zu den anderen Desktops). Dann muss man weder die Datei selbst durchsuchen noch die Zeile parsen. Wenn der Key nicht vorhanden ist, ist das Ergebnis leer.

Und vielen Dank für diese Serie, das ist echt cool.

Actionschnitzel
Geschrieben von Actionschnitzel am 28. September 2024 um 22:57

Na, das ist doch mal 'ne Ansage. Ich teste das und dann kommt's als Update in den nächsten Teil. Da merkt man aber auch, dass ich Gnome-Nutzer bin und von KDE keinen Plan hab.

MichaelM
Geschrieben von MichaelM am 29. September 2024 um 08:05

Macht ja nichts, dafür gibt es ja auch Leser. Ich hätte dafür mit Gnome das gleiche Problem :D Und ich habe das auch erst rausgefunden, weil ich eine bessere Lösung für mich gesucht habe.