commit 2dfdf54d4266d04e6e15306e3e72910fe25bc4ad Author: jonas H Date: Sun Aug 23 13:03:06 2026 +0200 initialise project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0972706 --- /dev/null +++ b/.gitignore @@ -0,0 +1,246 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +*.lcov +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi/* +!.pixi/config.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule* +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + + +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# rustc will dump stack traces when hitting an internal compiler error to PWD +rustc-ice-*.txt + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/funfacts.py b/funfacts.py new file mode 100644 index 0000000..7b5cb66 --- /dev/null +++ b/funfacts.py @@ -0,0 +1,702 @@ +#!/usr/bin/env python3 + +import random +import time +import json +import os +import subprocess +from datetime import datetime + + +# ============================================================ +# EINSTELLUNGEN +# ============================================================ + +# Durchschnittlicher Abstand zwischen zwei Funfacts +AVERAGE_HOURS = 2 + +# Niemals kürzer als diese Zeit warten +MIN_WAIT_MINUTES = 5 + +# Wie viele bereits gezeigte Facts gespeichert werden +HISTORY_SIZE = 200 + +# Datei für die History +HISTORY_FILE = os.path.expanduser( + "~/.tech_funfacts_history.json" +) + + +# ============================================================ +# FUNFACTS +# ============================================================ + +FUNFACTS = [ + + # ---------------- INTERNET ---------------- + + "Es gibt tatsächlich einen offiziellen Standard für Netzwerkpakete per Brieftaube: RFC 1149 aus dem Jahr 1990.", + + "Die erste registrierte Domain der Welt war symbolics.com. Sie wurde am 15. März 1985 registriert.", + + "Die erste E-Mail wurde 1971 von Ray Tomlinson verschickt.", + + "Ray Tomlinson verwendete das @-Zeichen, um Benutzer und Computer voneinander zu trennen.", + + "Das Internet und das World Wide Web sind nicht dasselbe. Das Web ist nur ein Dienst, der das Internet benutzt.", + + "Das World Wide Web wurde 1989 von Tim Berners-Lee vorgeschlagen.", + + "Die erste Website der Welt war info.cern.ch.", + + "Die erste Webcam wurde benutzt, um den Füllstand einer Kaffeemaschine zu überwachen.", + + "Das ARPANET war einer der wichtigsten Vorläufer des heutigen Internets.", + + "Der erste ARPANET-Versuch zwischen UCLA und Stanford sollte das Wort LOGIN übertragen. Nach den ersten zwei Buchstaben stürzte das System ab.", + + "IPv4 besitzt etwa 4,3 Milliarden mögliche Adressen.", + + "IPv6 besitzt ungefähr 340 Sextillionen mögliche Adressen.", + + "Eine IPv6-Adresse kann 128 Bit lang sein.", + + "DNS übersetzt normalerweise Domainnamen wie example.com in IP-Adressen.", + + "DNS steht für Domain Name System.", + + "HTTP steht für Hypertext Transfer Protocol.", + + "HTTPS verwendet normalerweise TLS, um die Verbindung zwischen Browser und Server zu verschlüsseln.", + + "Ein Ping verwendet normalerweise ICMP und nicht TCP.", + + "Der Begriff Spam für unerwünschte Nachrichten wurde durch Monty-Python-Sketches inspiriert.", + + "Der erste große Internet-Wurm hieß Morris Worm und verbreitete sich 1988.", + + + # ---------------- COMPUTER-GESCHICHTE ---------------- + + "Der erste bekannte Computer-Bug war tatsächlich eine Motte. Sie wurde 1947 aus einem Relais eines Harvard-Mark-II-Computers entfernt.", + + "Die erste Festplatte von IBM wurde 1956 vorgestellt.", + + "Die IBM 350 Disk Storage Unit von 1956 konnte ungefähr 5 Megabyte speichern.", + + "Die IBM 350 von 1956 wog ungefähr eine Tonne.", + + "Ein modernes Smartphone besitzt mehr Rechenleistung als viele Computer aus der Apollo-Ära.", + + "ENIAC wurde 1945 fertiggestellt und war einer der ersten großen elektronischen Universalcomputer.", + + "ENIAC verwendete ungefähr 18.000 Elektronenröhren.", + + "Die ersten Computer waren teilweise so groß, dass sie ganze Räume füllten.", + + "Der Begriff Computer bezeichnete ursprünglich tatsächlich Menschen, die Berechnungen durchführten.", + + "Konrad Zuse entwickelte mit der Z3 einen der ersten funktionsfähigen, programmierbaren Computer.", + + "Die Z3 von Konrad Zuse wurde 1941 fertiggestellt.", + + "Die Z3 verwendete Relais statt moderner Transistoren.", + + "Der erste kommerziell erfolgreiche Mikroprozessor war der Intel 4004.", + + "Der Intel 4004 erschien 1971.", + + "Der Intel 4004 war ein 4-Bit-Prozessor.", + + "Der Intel 4004 hatte nur etwa 2.300 Transistoren.", + + "Moderne CPUs besitzen dagegen Milliarden von Transistoren.", + + "Der Begriff CPU steht für Central Processing Unit.", + + "RAM steht für Random Access Memory.", + + "ROM steht für Read-Only Memory.", + + + # ---------------- HARDWARE ---------------- + + "Eine moderne CPU kann Milliarden Transistoren enthalten.", + + "Ein Transistor kann vereinfacht als elektronischer Schalter betrachtet werden.", + + "Moderne Prozessoren bestehen aus Milliarden winziger Schalter.", + + "CPUs werden häufig in Nanometern gefertigt. Diese Zahl beschreibt heute nicht einfach die tatsächliche Größe eines einzelnen Transistors.", + + "Eine CPU mit 3 GHz hat einen Takt von ungefähr drei Milliarden Zyklen pro Sekunde.", + + "GHz bedeutet Gigahertz.", + + "1 Gigahertz entspricht einer Milliarde Hertz.", + + "RAM verliert seinen Inhalt normalerweise, wenn die Stromversorgung abgeschaltet wird.", + + "SSDs besitzen keine beweglichen mechanischen Schreib-Leseköpfe.", + + "HDDs speichern Daten magnetisch auf rotierenden Scheiben.", + + "Eine klassische Festplatte kann mehrere rotierende Magnetscheiben enthalten.", + + "SSDs verwenden Flash-Speicher.", + + "Flash-Speicher kann Daten auch ohne permanente Stromversorgung behalten.", + + "USB steht für Universal Serial Bus.", + + "USB wurde unter anderem entwickelt, um die große Anzahl verschiedener PC-Anschlüsse zu reduzieren.", + + "USB-C beschreibt hauptsächlich einen Stecker und nicht automatisch die Geschwindigkeit oder Leistung eines Geräts.", + + "Ein USB-C-Anschluss kann je nach Gerät USB 2, USB 3, USB4, Thunderbolt oder andere Funktionen unterstützen.", + + "PCI Express wird häufig verwendet, um Grafikkarten und andere schnelle Erweiterungskarten anzubinden.", + + "NVMe wurde speziell für schnelle Speichergeräte über PCI Express entwickelt.", + + "Eine NVMe-SSD kommuniziert nicht einfach über SATA, sondern verwendet normalerweise PCI Express.", + + + # ---------------- LINUX ---------------- + + "Linux wurde 1991 von Linus Torvalds begonnen.", + + "Linux hieß ursprünglich nicht als fertiges Betriebssystem Linux. Linus Torvalds entwickelte zunächst den Kernel.", + + "Android verwendet den Linux-Kernel.", + + "Linux läuft auf Smartphones, Servern, Routern, Supercomputern, Fernsehern und vielen eingebetteten Geräten.", + + "Ein großer Teil der weltweit leistungsstärksten Supercomputer verwendet Linux.", + + "Der Linux-Kernel ist Open Source.", + + "GNU und Linux sind technisch gesehen unterschiedliche Projekte.", + + "Viele Linux-Distributionen verwenden GNU-Programme zusammen mit dem Linux-Kernel.", + + "Arch Linux verwendet standardmäßig den Paketmanager pacman.", + + "Der Linux-Befehl chmod kann die Zugriffsrechte einer Datei verändern.", + + "Der Befehl ps kann laufende Prozesse anzeigen.", + + "Der Befehl top kann laufende Prozesse und Systemauslastung anzeigen.", + + "htop ist eine interaktive Alternative zu top.", + + "Unter Linux wird nahezu alles als Datei oder über dateiähnliche Schnittstellen dargestellt.", + + "Die Verzeichnisstruktur von Linux beginnt bei /.", + + "Das Home-Verzeichnis eines Benutzers liegt normalerweise unter /home.", + + "Der Linux-Kernel kann während des Betriebs Informationen über /proc bereitstellen.", + + "Linux besitzt ein virtuelles Dateisystem namens sysfs.", + + "Viele Hardwareinformationen können unter Linux über virtuelle Dateisysteme ausgelesen werden.", + + "Ein Linux-System kann vollständig ohne grafische Oberfläche betrieben werden.", + + + # ---------------- NETZWERK ---------------- + + "Ein Ethernet-Kabel kann Daten übertragen, obwohl die Daten im Kabel als elektrische Signale dargestellt werden.", + + "WLAN steht für Wireless Local Area Network.", + + "LAN steht für Local Area Network.", + + "WAN steht für Wide Area Network.", + + "MAC-Adressen sind normalerweise 48 Bit lang.", + + "Eine IPv4-Adresse besteht aus 32 Bit.", + + "Eine IPv6-Adresse besteht aus 128 Bit.", + + "Ein /24-IPv4-Netz enthält theoretisch 256 Adressen.", + + "Bei einem normalen /24-Netz sind klassischerweise zwei Adressen für Netzwerk und Broadcast reserviert.", + + "127.0.0.1 bezeichnet normalerweise den eigenen Computer.", + + "Die Adresse 0.0.0.0 kann je nach Kontext eine besondere Bedeutung haben, beispielsweise als nicht spezifizierte Adresse.", + + "192.168.0.0/16 gehört zu den privaten IPv4-Adressbereichen.", + + "10.0.0.0/8 gehört zu den privaten IPv4-Adressbereichen.", + + "172.16.0.0/12 gehört zu den privaten IPv4-Adressbereichen.", + + "Router verbinden unterschiedliche Netzwerke miteinander.", + + "Switches verbinden Geräte innerhalb eines lokalen Netzwerks.", + + "Ein Switch arbeitet hauptsächlich auf Layer 2 des OSI-Modells.", + + "Router arbeiten hauptsächlich auf Layer 3 des OSI-Modells.", + + "TCP ist verbindungsorientiert.", + + "UDP ist verbindungslos und besitzt weniger Overhead als TCP.", + + + # ---------------- PROGRAMMIERUNG ---------------- + + "Python wurde nach der Comedy-Gruppe Monty Python benannt und nicht nach einer Schlange.", + + "Python wurde erstmals 1991 veröffentlicht.", + + "C wurde in den frühen 1970er-Jahren bei Bell Labs entwickelt.", + + "C++ entstand als Erweiterung der Programmiersprache C.", + + "JavaScript und Java sind trotz ihrer Namen unterschiedliche Programmiersprachen.", + + "HTML ist keine klassische Programmiersprache, sondern eine Auszeichnungssprache.", + + "CSS wird hauptsächlich verwendet, um das Aussehen von Webseiten zu beschreiben.", + + "SQL wird verwendet, um mit relationalen Datenbanken zu arbeiten.", + + "Git wurde ursprünglich von Linus Torvalds entwickelt.", + + "Git wurde ursprünglich für die Entwicklung des Linux-Kernels entwickelt.", + + "Ein Compiler übersetzt Quellcode normalerweise in eine andere Form, beispielsweise Maschinencode oder Bytecode.", + + "Ein Interpreter führt Code vereinfacht gesagt direkt bzw. über eine Laufzeitumgebung aus.", + + "Ein Algorithmus ist eine eindeutig beschriebene Vorgehensweise zur Lösung eines Problems.", + + "Ein Bit kann grundsätzlich zwei Zustände darstellen: 0 oder 1.", + + "Ein Byte besteht normalerweise aus 8 Bit.", + + "1 Kilobyte kann je nach Definition 1.000 oder 1.024 Bytes bedeuten.", + + "1 Kibibyte entspricht exakt 1.024 Bytes.", + + "1 Megabyte entspricht im SI-System 1.000.000 Bytes.", + + "1 MiB entspricht exakt 1.048.576 Bytes.", + + "Binärzahlen verwenden nur die Ziffern 0 und 1.", + + + # ---------------- ZEIT / DATEN ---------------- + + "Unix-Zeit zählt normalerweise Sekunden seit dem 1. Januar 1970.", + + "Der sogenannte Year-2038-Bug betrifft Systeme, die Unix-Zeit in einem signierten 32-Bit-Wert speichern.", + + "Am 19. Januar 2038 erreicht ein klassischer signierter 32-Bit-Unix-Zeitstempel sein Maximum.", + + "Ein 64-Bit-Unix-Zeitstempel kann für sehr lange Zeiträume verwendet werden.", + + "Computer speichern Zeit häufig nicht so, wie Menschen sie auf einer Uhr sehen.", + + "Zeitzonen machen Zeitberechnungen in Computersystemen deutlich komplizierter.", + + "UTC steht für Coordinated Universal Time.", + + "NTP kann verwendet werden, um Computeruhren über Netzwerke zu synchronisieren.", + + "GPS-Satelliten benötigen extrem genaue Zeitmessung.", + + "Schaltsekunden können bei Computersystemen zu unerwarteten Problemen führen.", + + + # ---------------- SICHERHEIT ---------------- + + "Ein Passwort muss nicht nur lang, sondern auch möglichst einzigartig sein.", + + "Passwort-Hashing ist etwas anderes als Verschlüsselung.", + + "Ein guter Passwort-Hash soll nicht einfach wieder in das ursprüngliche Passwort zurückgerechnet werden können.", + + "Salts werden beim Passwort-Hashing verwendet, damit identische Passwörter nicht denselben Hash erzeugen.", + + "HTTPS bedeutet nicht automatisch, dass eine Website vertrauenswürdig ist. Es schützt hauptsächlich die Verbindung.", + + "Ein Vorhängeschloss im Browser bedeutet nicht automatisch, dass eine Website seriös ist.", + + "Zwei-Faktor-Authentifizierung kann einen zusätzlichen Schutz bieten, selbst wenn ein Passwort bekannt wird.", + + "SSH steht für Secure Shell.", + + "SSH wird häufig verwendet, um Linux-Server remote zu verwalten.", + + "Ein SSH-Schlüssel kann zur Authentifizierung verwendet werden, ohne jedes Mal ein Passwort einzugeben.", + + + # ---------------- DATEIEN / SPEICHER ---------------- + + "Eine Datei mit der Endung .jpg muss nicht zwingend ein Bild sein. Die Endung ist nur eine Namenskonvention.", + + "ZIP-Dateien können mehrere Dateien und Verzeichnisse in einem Archiv zusammenfassen.", + + "Kompression kann Daten kleiner machen, ohne sie zu verändern, oder Informationen entfernen, um noch kleinere Dateien zu erzeugen.", + + "PNG verwendet verlustfreie Kompression.", + + "JPEG verwendet normalerweise verlustbehaftete Kompression.", + + "FLAC ist ein verlustfreies Audioformat.", + + "MP3 verwendet verlustbehaftete Audiokompression.", + + "Eine Datei kann gelöscht werden, obwohl ihre Daten auf einem Datenträger noch nicht sofort überschrieben wurden.", + + "Dateisysteme speichern zusätzliche Informationen wie Dateigröße, Besitzer und Änderungszeit.", + + "ext4 ist ein weit verbreitetes Linux-Dateisystem.", + + + # ---------------- WEIRD / FUN ---------------- + + "Es gibt einen offiziellen Internetstandard für die Übertragung von IP-Paketen mit Brieftauben.", + + "Ein Computer kann theoretisch eine komplette Festplatte mit Nullen überschreiben, obwohl diese Nullen keine sichtbaren Informationen enthalten.", + + "Der Name CAPTCHA steht für Completely Automated Public Turing test to tell Computers and Humans Apart.", + + "CAPTCHAs wurden entwickelt, um Menschen und automatisierte Programme voneinander zu unterscheiden.", + + "QR-Codes können auch dann noch funktionieren, wenn ein Teil des Codes beschädigt ist.", + + "QR-Codes wurden ursprünglich für die Automobilindustrie entwickelt.", + + "Der erste Computermonitor war noch weit von heutigen 4K-Displays entfernt.", + + "Frühe Computerspiele konnten mit wenigen Kilobyte auskommen.", + + "Das Spiel DOOM von 1993 war klein genug, dass seine Größe nach heutigen Maßstäben geradezu lächerlich wirkt.", + + "Tetris wurde ursprünglich 1984 von Alexei Paschitnow entwickelt.", + + "Der Game-Boy-Prozessor lief nur mit wenigen Megahertz.", + + "Moderne Grafikkarten besitzen tausende Recheneinheiten.", + + "Eine GPU kann viele Berechnungen parallel durchführen und ist deshalb besonders gut für bestimmte mathematische Aufgaben geeignet.", + + "Minecraft-Welten können extrem große Koordinatenbereiche besitzen.", + + "Ein Pixel ist ein einzelnes Bildelement eines digitalen Bildes.", + + "4K bedeutet bei Video nicht immer exakt dieselbe Auflösung. Im TV-Bereich ist 3840×2160 üblich.", + + "Ein 1080p-Bild besitzt ungefähr zwei Millionen Pixel.", + + "Ein 4K-UHD-Bild besitzt ungefähr acht Millionen Pixel.", + + "Ein 8K-UHD-Bild besitzt ungefähr 33 Millionen Pixel.", + + + # ---------------- WEITERE TECH-FACTS ---------------- + + "Bluetooth und WLAN verwenden beide Funktechnik, sind aber für unterschiedliche Einsatzzwecke optimiert.", + + "Bluetooth wurde ursprünglich nach Harald Blauzahn, einem dänischen König, benannt.", + + "Das Bluetooth-Logo kombiniert Runen, die für Harald Blauzahn stehen.", + + "ASCII verwendet ursprünglich 7 Bit pro Zeichen.", + + "ASCII kann deshalb 128 verschiedene Werte darstellen.", + + "Unicode wurde entwickelt, um Zeichen aus sehr vielen Schriftsystemen darstellen zu können.", + + "UTF-8 kann ASCII-Zeichen direkt darstellen.", + + "UTF-8 ist heute eine der wichtigsten Zeichenkodierungen im Internet.", + + "Ein Emoji ist technisch gesehen nicht einfach nur ein einzelnes Byte.", + + "Die Zahl 255 ist in Binärdarstellung 11111111.", + + "Die Zahl 256 ist 2 hoch 8.", + + "Ein 32-Bit-Wert kann 2 hoch 32 verschiedene Bitmuster darstellen.", + + "Ein 64-Bit-Wert kann 2 hoch 64 verschiedene Bitmuster darstellen.", + + "2 hoch 64 ist 18.446.744.073.709.551.616.", + + "Ein einzelnes Bit ist die kleinste klassische Informationseinheit in einem Binärsystem.", + + "Computer verwenden Binärlogik, weil elektronische Schaltungen besonders gut zwischen zwei Zuständen unterscheiden können.", + + "Ein CPU-Cache ist deutlich schneller als der normale Arbeitsspeicher, aber auch wesentlich kleiner.", + + "L1-Cache ist normalerweise kleiner und schneller als L2-Cache.", + + "L3-Cache ist normalerweise größer und langsamer als L1-Cache.", + + "Moderne CPUs können mehrere Cache-Ebenen besitzen.", + + + # ---------------- DYNAMIC FACTS ---------------- + + "Seit dem Start des Unix-Zeitstempels am 1. Januar 1970 sind bereits {unix_days} Tage vergangen.", + + "Seit der ersten E-Mail von Ray Tomlinson sind ungefähr {email_days} Tage vergangen.", + + "Die erste E-Mail wurde 1971 verschickt. Das ist inzwischen mehr als ein halbes Jahrhundert her.", + + "Der Unix-Zeitstempel startete am 1. Januar 1970. Heute sind bereits {unix_seconds} Sekunden seit diesem Zeitpunkt vergangen.", + + "Bis zum Year-2038-Problem sind es noch ungefähr {days_until_2038} Tage.", + + "Der erste Webserver wurde am CERN betrieben. Das World Wide Web ist inzwischen Jahrzehnte alt.", + +] + + +# ============================================================ +# DYNAMISCHE FAKTEN +# ============================================================ + +def replace_dynamic(text): + + now = datetime.now() + + # Unix-Tage + unix_start = datetime(1970, 1, 1) + unix_seconds = int((now - unix_start).total_seconds()) + unix_days = (now - unix_start).days + + # Erste E-Mail + # Ray Tomlinsons erste Netzwerknachricht wird meist auf 1971 datiert. + email_date = datetime(1971, 10, 29) + email_days = (now - email_date).days + + # Year 2038 + y2038 = datetime(2038, 1, 19, 3, 14, 7) + days_until_2038 = (y2038 - now).days + + replacements = { + "{unix_seconds}": f"{unix_seconds:,}".replace(",", "."), + "{unix_days}": f"{unix_days:,}".replace(",", "."), + "{email_days}": f"{email_days:,}".replace(",", "."), + "{days_until_2038}": f"{days_until_2038:,}".replace(",", "."), + } + + for placeholder, value in replacements.items(): + text = text.replace(placeholder, value) + + return text + + +# ============================================================ +# HISTORY LADEN +# ============================================================ + +def load_history(): + + if not os.path.exists(HISTORY_FILE): + return [] + + try: + with open(HISTORY_FILE, "r", encoding="utf-8") as file: + data = json.load(file) + + if isinstance(data, list): + return data + + except (json.JSONDecodeError, OSError): + pass + + return [] + + +# ============================================================ +# HISTORY SPEICHERN +# ============================================================ + +def save_history(history): + + try: + with open(HISTORY_FILE, "w", encoding="utf-8") as file: + json.dump( + history, + file, + ensure_ascii=False, + indent=2 + ) + + except OSError as error: + print(f"Fehler beim Speichern der History: {error}") + + +# ============================================================ +# NOTIFICATION +# ============================================================ + +def notify(text): + + try: + + subprocess.run( + [ + "notify-send", + "IT Funfact", + text + ], + check=False + ) + + except FileNotFoundError: + + print( + "FEHLER: notify-send wurde nicht gefunden." + ) + + except Exception as error: + + print( + f"Fehler beim Anzeigen der Notification: {error}" + ) + + +# ============================================================ +# ZUFÄLLIGE WARTEZEIT +# ============================================================ + +def random_wait(): + + # Exponentialverteilung: + # + # Durchschnitt = 6 Stunden + # + # Dadurch entstehen absichtlich sehr unterschiedliche + # Abstände. + # + # Beispiel: + # 20 Minuten + # 2 Stunden + # 7 Stunden + # 13 Stunden + # usw. + # + # Extrem lange/kürzere Abstände sind möglich. + + average_seconds = AVERAGE_HOURS * 60 * 60 + + seconds = random.expovariate( + 1 / average_seconds + ) + + minimum_seconds = MIN_WAIT_MINUTES * 60 + + return max( + seconds, + minimum_seconds + ) + + +# ============================================================ +# EINEN ZUFÄLLIGEN FACT AUSWÄHLEN +# ============================================================ + +def choose_fact(history): + + # Bereits angezeigte Facts vermeiden + available = [ + fact + for fact in FUNFACTS + if fact not in history + ] + + # Wenn alle Facts irgendwann angezeigt wurden: + # History zurücksetzen. + if not available: + + print( + "Alle Funfacts wurden bereits angezeigt." + ) + + history.clear() + + available = FUNFACTS + + return random.choice(available) + + +# ============================================================ +# HAUPTPROGRAMM +# ============================================================ + +def main(): + + print() + print("=" * 60) + print(" IT FUNFACTS") + print("=" * 60) + print() + print(f"Funfacts insgesamt: {len(FUNFACTS)}") + print(f"Durchschnittliches Intervall: {AVERAGE_HOURS} Stunden") + print(f"History: {HISTORY_FILE}") + print() + print("Programm läuft...") + print("Mit Ctrl+C beenden.") + print() + + history = load_history() + + try: + + while True: + + wait_time = random_wait() + + print( + f"Nächster Funfact in " + f"{wait_time / 3600:.2f} Stunden." + ) + + time.sleep(wait_time) + + fact = choose_fact(history) + + fact = replace_dynamic(fact) + + print() + print("FUNFACT:") + print(fact) + print() + + notify(fact) + + # Den fertigen Text speichern. + # Dadurch wird auch verhindert, + # dass derselbe dynamische Fact direkt + # wieder erscheint. + history.append(fact) + + # History begrenzen + history = history[-HISTORY_SIZE:] + + save_history(history) + + except KeyboardInterrupt: + + print() + print("Funfacts beendet.") + print() + + +# ============================================================ +# START +# ============================================================ + +if __name__ == "__main__": + main() diff --git a/lookup serverjonas.de b/lookup serverjonas.de new file mode 100644 index 0000000..b59573d --- /dev/null +++ b/lookup serverjonas.de @@ -0,0 +1,2 @@ +zsh: command not found: dig + diff --git a/main.py b/main.py new file mode 100755 index 0000000..346bb1e --- /dev/null +++ b/main.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +print("test") +print("main.py wurde gestartet") diff --git a/myrummy/READEME.txt b/myrummy/READEME.txt new file mode 100644 index 0000000..82e0b3a --- /dev/null +++ b/myrummy/READEME.txt @@ -0,0 +1,46 @@ +MYRUMMY CLI + +Start: + python main.py + +Beim Start werden zwei Einstellungen abgefragt: + - 13-1-Umlauf: Folgen dürfen über 13 hinweg weiterlaufen (z.B. 12,13,1) + - Erstmeldung 30: Für die erste Meldung werden 30 Punkte gebraucht + +Nach jedem beendeten Zug erscheint ein Übergabe-Bildschirm, damit der +nächste Spieler die Karten des Vorgängers nicht sieht. + +Farben sind absichtlich nur ANSI-Farben + Zahlen: + Rot / Gelb / Grün / Blau + Joker = J + +Der Tisch darf während eines Zuges beliebig umgebaut werden. Erst `finish` +prüft alle Gruppen. Dadurch muss der Editor keine gültigen Zwischenzustände +erzwingen. + +Befehle: + m H G Handkarte H in Tischgruppe G + m H new Handkarte H in neue Gruppe + t G C Tischkarte C aus Gruppe G zurück in Hand + n neue leere Gruppe + a G1 G2 Gruppen verbinden + s G POS Gruppe nach POS teilen + d Karte ziehen + u letzte Änderung rückgängig + sort Hand sortieren + finish Zug prüfen/beenden + q beenden + +Regeln der Implementierung: + - 2 komplette Kartensätze + - 4 Farben, Werte 1-13 + - 4 Joker + - Satz: gleiche Zahl, unterschiedliche Farben, 3-4 Karten + - Folge: gleiche Farbe, aufsteigend, 3+ Karten + - Joker dürfen in Folgen Lücken füllen + - Erstmeldung: mindestens 30 Punkte + - Wer keine Handkarten mehr hat, gewinnt + +Hinweis: Das ist eine bewusst einfache CLI-Version. Die Regeln für Sonderfälle +wie Joker-Rücknahme, 1-13-Runden, Punktwertung nach Spielende usw. können später +noch ergänzt werden. diff --git a/myrummy/cards.py b/myrummy/cards.py new file mode 100644 index 0000000..eb091cd --- /dev/null +++ b/myrummy/cards.py @@ -0,0 +1,75 @@ +from dataclasses import dataclass + +RESET='\033[0m' +COLORS={'r':'\033[31m','y':'\033[33m','g':'\033[32m','b':'\033[34m'} +COLOR_ORDER={'r':0,'y':1,'g':2,'b':3} + +@dataclass(frozen=True) +class Card: + color: str|None + value: int|None + joker: bool=False + uid: int=0 + + def text(self): + if self.joker: + return f'{COLORS["r"]}J{RESET}' + return f'{COLORS[self.color]}{self.value}{RESET}' + + def plain(self): + return 'J' if self.joker else f'{self.color}{self.value}' + + +def deck(): + cards=[] + uid=0 + for _ in range(2): + for c in COLORS: + for n in range(1,14): + cards.append(Card(c,n,False,uid)); uid+=1 + for _ in range(2): + cards.append(Card(None,None,True,uid)); uid+=1 + return cards + + +def points(card): + return 30 if card.joker else card.value + + +def valid_group(cards): + if not 3 <= len(cards) <= 4: return False + normal=[c for c in cards if not c.joker] + jok=len(cards)-len(normal) + if not normal: return False + return len({c.value for c in normal})==1 and len({c.color for c in normal})==len(normal) and len(normal)+jok>=3 + + +def valid_run(cards, wrap=False): + if not 3 <= len(cards) <= 13: return False + normal=sorted((c for c in cards if not c.joker), key=lambda x:x.value) + jok=len(cards)-len(normal) + if not normal or len({c.color for c in normal}) != 1: return False + if len({c.value for c in normal}) != len(normal): return False + if not wrap: + gaps=sum(b.value-a.value-1 for a,b in zip(normal,normal[1:])) + return gaps <= jok and normal[-1].value-normal[0].value+1+jok <= 13 + # 13-1-Umlauf: Werte liegen auf einem Kreis 1..13, die Folge ist ein + # zusammenhängender Bogen. Alle Lücken außer der größten müssen durch + # Joker gefüllt werden. + gaps=[b.value-a.value-1 for a,b in zip(normal,normal[1:])] + gaps.append(normal[0].value+13-normal[-1].value-1) + return 13-len(normal)-jok <= max(gaps) + + +def valid(cards, wrap=False): + return valid_group(cards) or valid_run(cards, wrap) + + +def kind(cards, wrap=False): + if valid_run(cards, wrap): return 'Folge' + if valid_group(cards): return 'Satz' + return 'UNGÜLTIG' + + +def sort_hand(cards): + return sorted(cards,key=lambda c:(c.joker, COLOR_ORDER.get(c.color,99), c.value or 99)) diff --git a/myrummy/game.py b/myrummy/game.py new file mode 100644 index 0000000..45f42a8 --- /dev/null +++ b/myrummy/game.py @@ -0,0 +1,159 @@ +import copy, random +from cards import * + +def yes_no(q, default=None): + while True: + a=input(q+' (j/n): ').strip().lower() + if a in ('j','ja','y','yes'): return True + if a in ('n','nein','no'): return False + if not a and default is not None: return default + print(' Bitte j oder n eingeben.') + +class Player: + def __init__(self,name): + self.name=name + self.hand=[] + self.opened=False + +class Game: + def __init__(self): + print('EINSTELLUNGEN') + print(' ~~~~~~~~~~~~') + self.wrap=yes_no(' 13-1-Umlauf erlauben (z.B. 12,13,1)') + self.min30=yes_no(' Braucht die Erstmeldung 30 Punkte') + self.players=[Player('Spieler 1'),Player('Spieler 2')] + self.turn=0 + self.table=[] + self.deck=deck(); random.shuffle(self.deck) + self.history=[] + self.drew=False + self.handover=False + for p in self.players: + p.hand=sort_hand([self.deck.pop() for _ in range(14)]) + + @property + def p(self): return self.players[self.turn] + + def save(self): + self.history.append(copy.deepcopy((self.players,self.table,self.deck,self.drew))) + + def undo(self): + if not self.history: + print('Nichts zum Rückgängigmachen.'); return + self.players,self.table,self.deck,self.drew=self.history.pop() + + def clear(self): print('\033[2J\033[H',end='') + + def show(self): + self.clear() + print('╔════════════════════════════════════════════════════╗') + print('║ MYRUMMY ║') + print('╚════════════════════════════════════════════════════╝') + print(f' Am Zug: {self.p.name} Karten im Stapel: {len(self.deck)}') + print(f' Regeln: 13-1-Umlauf: {"ja" if self.wrap else "nein"} Erstmeldung 30: {"ja" if self.min30 else "nein"}') + print(f' Erstmeldung: {"ja" if self.p.opened else "nein"}') + print('\nTISCH') + if not self.table: print(' (leer)') + for i,g in enumerate(self.table,1): + print(f' {i:>2}: ' + ' '.join(f'{j}:{c.text()}' for j,c in enumerate(g,1)) + f' [{kind(g, self.wrap)}]') + print('\nHAND') + lines=[]; key=None + for i,c in enumerate(self.p.hand,1): + k='J' if c.joker else c.color + if k!=key: lines.append([]); key=k + lines[-1].append(f'{i}:{c.text()}') + for l in lines: print(' '+ ' '.join(l)) + print('\nBEFEHLE') + print(' m H G Handkarte H -> Gruppe G') + print(' m H new Handkarte H -> neue Gruppe') + print(' t G C Tischkarte C aus Gruppe G -> Hand') + print(' n leere neue Gruppe') + print(' a G1 G2 Gruppen verbinden') + print(' s G POS Gruppe nach POS teilen') + print(' d ziehen') + print(' u undo') + print(' sort Hand sortieren') + print(' finish Zug prüfen/beenden') + print(' q beenden') + + def idx(self,x,maxi): + n=int(x) + if not 1<=n<=maxi: raise ValueError('Nummer außerhalb des gültigen Bereichs.') + return n-1 + + def move_hand(self,h,g): + hi=self.idx(h,len(self.p.hand)); self.save() + card=self.p.hand.pop(hi) + if g.lower()=='new': self.table.append([card]) + else: self.table[self.idx(g,len(self.table))].append(card) + + def move_table(self,g,c): + gi=self.idx(g,len(self.table)); ci=self.idx(c,len(self.table[gi])); self.save() + self.p.hand.append(self.table[gi].pop(ci)) + if not self.table[gi]: self.table.pop(gi) + self.p.hand=sort_hand(self.p.hand) + + def merge(self,a,b): + ai=self.idx(a,len(self.table)); bi=self.idx(b,len(self.table)) + if ai==bi: raise ValueError('Gleiche Gruppe.') + self.save(); self.table[ai]+=self.table[bi]; self.table.pop(bi) + + def split(self,g,pos): + gi=self.idx(g,len(self.table)); pos=int(pos) + if not 1<=pos ')): return + if self.handover: + self.handover=False + self.clear() + print(f'\n Zug beendet. Bitte gib den Computer an {self.p.name} weiter,') + print(' damit der andere Spieler deine Karten nicht sehen kann.') + input(' ENTER drücken, wenn du bereit bist...') diff --git a/myrummy/main.py b/myrummy/main.py new file mode 100644 index 0000000..263a07b --- /dev/null +++ b/myrummy/main.py @@ -0,0 +1,4 @@ +from game import Game + +if __name__ == '__main__': + Game().run() diff --git a/prg/SJNet_filemanager.py b/prg/SJNet_filemanager.py new file mode 100644 index 0000000..0467fea --- /dev/null +++ b/prg/SJNet_filemanager.py @@ -0,0 +1,646 @@ +#!/usr/bin/env python3 +"""SJN File Manager - a TUI file manager. + +Keys: + Up/Down or k/j Navigate + Enter Open (folder -> cd, file -> menu) + / Search (live-filter) + Backspace or h Go to parent directory + . Toggle hidden files + q Quit +""" + +import curses +import os +import shlex +import subprocess +from pathlib import Path + + +PARENT = Path("..") + + +# ---------- helpers --------------------------------------------------------- + + +FILE_CATEGORIES = { + "Bilder": { + ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg" + }, + "Videos": { + ".mp4", ".mkv", ".avi", ".mov", ".webm" + }, + "Musik": { + ".mp3", ".wav", ".flac", ".ogg", ".m4a" + }, + "Dokumente": { + ".pdf", ".doc", ".docx", ".odt", ".txt", ".rtf" + }, + "Tabellen": { + ".xls", ".xlsx", ".ods", ".csv" + }, + "Präsentationen": { + ".ppt", ".pptx", ".odp" + }, + "Code": { + ".py", ".js", ".ts", ".java", ".c", ".cpp", ".h", + ".rs", ".go", ".php", ".sh" + }, + "Archive": { + ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar" + }, +} + + + +def get_category(filename): + extension = Path(filename).suffix.lower() + + for category, extensions in FILE_CATEGORIES.items(): + if extension in extensions: + return category + + return "Sonstige" + + + +def human_size(n: int) -> str: + """Return a human-readable file size.""" + units = ["B", "K", "M", "G", "T"] + size = float(n) + for unit in units: + if size < 1024 or unit == units[-1]: + if unit == "B": + return f"{int(size)}{unit}" + return f"{size:.1f}{unit}" + size /= 1024 + return f"{int(size)}T" + + +def list_entries(cwd: Path, show_hidden: bool): + """Return [.., dir1, dir2, ..., file1, file2, ...].""" + try: + all_entries = list(cwd.iterdir()) + except PermissionError: + return [PARENT] + + if not show_hidden: + all_entries = [e for e in all_entries if not e.name.startswith(".")] + + dirs = sorted( + (e for e in all_entries if e.is_dir(follow_symlinks=False)), + key=lambda e: e.name.lower(), + ) + files = sorted( + (e for e in all_entries if e.is_file(follow_symlinks=False)), + key=lambda e: e.name.lower(), + ) + return [PARENT] + dirs + files + + +def is_parent(entry: Path) -> bool: + return entry == PARENT + + +def format_entry(name: str, is_dir: bool, full_path: Path, width: int) -> str: + """Format an entry for display, padded to width.""" + if is_dir: + label = f"[D] {name}/" + else: + try: + label = f"[F] {name} ({human_size(full_path.stat().st_size)})" + except OSError: + label = f"[F] {name}" + return label[:width].ljust(width) + + +# ---------- input ------------------------------------------------------------ + +def prompt_line(stdscr, prompt: str, initial: str = "") -> str | None: + """Show a prompt at the bottom and read a line. Returns None if cancelled (ESC). + + Modes are restored in `finally` so a curses.error or any other exception + cannot leave the terminal in a half-configured state. + """ + max_y, max_x = stdscr.getmaxyx() + row = max_y - 2 + + curses.curs_set(1) + curses.echo() + # Keep keypad ON so arrow keys arrive as KEY_* constants instead of an + # ESC prefix that would otherwise be mis-read as "user pressed cancel". + try: + stdscr.addstr(row, 0, prompt) + stdscr.clrtoeol() + stdscr.refresh() + + buf = initial + while True: + ch = stdscr.getch() + if ch == 27: # bare ESC (no following escape-sequence bytes) + return None + if ch in (10, 13, curses.KEY_ENTER): + break + if ch in (curses.KEY_BACKSPACE, 127, 8): + if buf: + buf = buf[:-1] + elif 0 <= ch < 256: + # Plain ASCII printable; safe to append. + buf += chr(ch) + # Arrow keys, function keys, etc. are silently ignored. + + stdscr.addstr(row, 0, prompt + (buf or "")) + stdscr.clrtoeol() + stdscr.refresh() + + return buf + finally: + curses.noecho() + curses.curs_set(0) + try: + stdscr.touchwin() + stdscr.refresh() + except curses.error: + pass + + +# ---------- rendering ------------------------------------------------------- + +def init_colors() -> dict[str, int]: + """Initialize color pairs and return a mapping of name -> pair id.""" + pairs = { + "dir": 1, + "file": 2, + "selected": 3, + "selected_dir": 4, + "status_ok": 5, + "status_err": 6, + "header": 7, + } + if not curses.has_colors(): + return {} + curses.start_color() + curses.use_default_colors() + try: + curses.init_pair(1, curses.COLOR_CYAN, -1) + curses.init_pair(2, curses.COLOR_WHITE, -1) + curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) + curses.init_pair(4, curses.COLOR_CYAN, curses.COLOR_WHITE) + curses.init_pair(5, curses.COLOR_GREEN, -1) + curses.init_pair(6, curses.COLOR_RED, -1) + curses.init_pair(7, curses.COLOR_YELLOW, -1) + except curses.error: + pass + return pairs + + +def draw(stdscr, entries, filtered, selected, cwd, status, status_kind, show_hidden, query, colors): + stdscr.erase() + max_y, max_x = stdscr.getmaxyx() + + visible = filtered if filtered is not None else entries + + # Header + hidden_flag = " (hidden shown)" if show_hidden else "" + header = f" {cwd}{hidden_flag} " + if colors: + stdscr.attron(curses.color_pair(colors["header"]) | curses.A_BOLD) + try: + stdscr.addstr(0, 0, header[: max_x - 1]) + except curses.error: + pass + if colors: + stdscr.attroff(curses.color_pair(colors["header"]) | curses.A_BOLD) + + # List area + list_top = 1 + list_bottom = max_y - 2 # leave one row for status, one for footer + rows = max(0, list_bottom - list_top) + + # Clamp selection to a valid index in `visible`. + if visible: + selected = max(0, min(selected, len(visible) - 1)) + else: + selected = 0 + + if rows <= 0: + # Terminal too small to render anything meaningful. + stdscr.refresh() + return selected + + # Scroll window + start = 0 + if selected >= rows: + start = selected - rows + 1 + + end = min(start + rows, len(visible)) + for entry_i in range(start, end): + entry = visible[entry_i] + if is_parent(entry): + name = ".." + is_dir = True + full = cwd.parent + else: + name = entry.name + is_dir = entry.is_dir(follow_symlinks=False) + full = entry + + label = format_entry(name, is_dir, full, max_x - 1) + + attr = curses.A_NORMAL + if colors: + if entry_i == selected: + attr = curses.color_pair( + colors["selected_dir"] if is_dir else colors["selected"] + ) | curses.A_BOLD + else: + attr = curses.color_pair( + colors["dir"] if is_dir else colors["file"] + ) + else: + if entry_i == selected: + attr = curses.A_REVERSE + elif is_dir: + attr = curses.A_BOLD + + line_y = list_top + (entry_i - start) + if line_y < list_bottom: + try: + stdscr.addnstr(line_y, 0, label, max_x - 1, attr) + except curses.error: + pass + + # Status line (one row above footer) + status_y = max_y - 2 + status_text = "" + status_attr = curses.A_DIM + if status: + status_text = status + elif query is not None: + status_text = f"/ {query}" + if status_text and colors and status_kind in colors: + status_attr = curses.color_pair(colors[status_kind]) | curses.A_BOLD + try: + if status_text: + stdscr.addnstr(status_y, 0, status_text, max_x - 1, status_attr) + except curses.error: + pass + + # Footer (key hints) + footer = " Up/Dn move | Enter open | / search | .. parent | . hidden | q quit " + try: + stdscr.addnstr(max_y - 1, 0, footer, max_x - 1, curses.A_DIM) + except curses.error: + pass + + stdscr.refresh() + return selected + + +# ---------- file type / open-with configuration ----------------------------- + +OPEN_COMMANDS = { + "Bilder": [ + ("feh", ["feh", "{file}"]), + ("imv", ["imv", "{file}"]), + ], + + "Videos": [ + ("mpv", ["mpv", "{file}"]), + ("vlc", ["vlc", "{file}"]), + ("show", ["show", "{file}"]), + ("newshow", ["newshow", "{file}"]), + ], + + "Musik": [ + ("mpv", ["mpv", "{file}"]), + ("ffplay", ["ffplay", "{file}"]), + ], + + "Dokumente": [ + ("LibreOffice", ["libreoffice", "{file}"]), + ("firefox", ["firefox", "{file}"]), + ], + + "PDF": [ + ("zathura", ["zathura", "{file}"]), + ("firefox", ["firefox", "{file}"]), + ], + + "Code": [ + ("nano", ["nano", "{file}"]), + ("helix", ["helix", "{file}"]), + ], + + "Archive": [ + ("File Roller", ["file-roller", "{file}"]), + ], + + "Sonstige": [ + ("nano", ["nano", "{file}"]), + ], +} + + +def file_menu(stdscr, filepath: Path, colors) -> list[str] | None: + """Show the open-with menu based on the file category. + Returns argv list, or None if cancelled. + """ + + category = get_category(filepath) + + # Get commands for this category. + # If the category does not exist, fall back to "Sonstige". + commands = OPEN_COMMANDS.get( + category, + OPEN_COMMANDS.get("Sonstige", []) + ) + + stdscr.erase() + max_y, max_x = stdscr.getmaxyx() + + lines = [ + f"Open: {filepath.name}", + f"Type: {category}", + "", + ] + + # Add category-specific programs. + for number, (name, _argv) in enumerate(commands, start=1): + lines.append(f" {number}) {name}") + + # Custom command is always available. + custom_number = len(commands) + 1 + + lines.extend([ + f" {custom_number}) custom command", + "", + f"Press 1-{custom_number} - ESC to cancel", + ]) + + for i, line in enumerate(lines): + if i >= max_y - 1: + break + + try: + if colors: + stdscr.addnstr( + i, + 2, + line, + max_x - 3, + curses.color_pair(colors["header"]) | curses.A_BOLD, + ) + else: + stdscr.addnstr( + i, + 2, + line, + max_x - 3, + curses.A_BOLD, + ) + except curses.error: + pass + + stdscr.refresh() + + while True: + ch = stdscr.getch() + + if ch == 27: # ESC + return None + + if ord("1") <= ch <= ord("9"): + number = ch - ord("0") + + # Predefined command + if 1 <= number <= len(commands): + _name, argv = commands[number - 1] + + return [ + arg.replace("{file}", str(filepath)) + for arg in argv + ] + + # Custom command + if number == custom_number: + cmd = prompt_line( + stdscr, + "Command (use {{file}} for path): " + ) + + if cmd is None or not cmd.strip(): + return None + + if "{file}" in cmd: + resolved = cmd.replace( + "{file}", + str(filepath) + ) + else: + resolved = cmd + " " + str(filepath) + + try: + return shlex.split(resolved) + except ValueError: + return resolved.split() + + + +def run_external(stdscr, argv: list[str]) -> str: + """Suspend curses, run argv, then restore. Returns a status string. + + Defensive: we snapshot cwd before, run, then forcibly chdir back to the + snapshot afterwards. Even though `subprocess.run` with a list argv does + not invoke a shell and therefore cannot change the parent's cwd, if a + future change ever switches to `shell=True` this guarantees we stay put. + """ + saved_cwd = os.getcwd() + curses.def_prog_mode() + curses.endwin() + status = "" + try: + try: + result = subprocess.run(argv) + if result.returncode == 0: + status = f"Ran: {' '.join(argv)}" + else: + status = f"Exit {result.returncode}: {' '.join(argv)}" + except FileNotFoundError: + status = f"Command not found: {argv[0]}" + except KeyboardInterrupt: + status = "Interrupted" + except Exception as e: # noqa: BLE001 + status = f"Error: {e}" + finally: + # Always restore cwd before resuming curses. + try: + os.chdir(saved_cwd) + except OSError: + pass + curses.reset_prog_mode() + try: + stdscr.redrawwin() + stdscr.refresh() + except curses.error: + pass + return status + + +# ---------- main loop ------------------------------------------------------- + +def main(stdscr): + colors = init_colors() + curses.curs_set(0) + stdscr.keypad(True) + + cwd = Path.cwd() + show_hidden = False + + selected = 0 + status = "" + status_kind = "status_ok" + query = None + filtered = None # when not None, used for display (search active) + + while True: + try: + entries = list_entries(cwd, show_hidden) + + if filtered is not None: + # Re-apply filter against the fresh entries + if query is None: + filtered = None + else: + qlow = query.lower() + candidates = [e for e in entries if qlow in e.name.lower()] + if not candidates: + filtered = [] + else: + if candidates[0] != PARENT and PARENT in entries: + candidates = [PARENT] + [ + e for e in candidates if e != PARENT + ] + filtered = candidates + + selected = draw( + stdscr, + entries, + filtered, + selected, + cwd, + status, + status_kind, + show_hidden, + query, + colors, + ) + + status = "" + status_kind = "status_ok" + + ch = stdscr.getch() + + if ch in (ord("q"),): + break + + if ch in (curses.KEY_UP, ord("k")): + vis = filtered if filtered is not None else entries + if vis: + selected = (selected - 1) % len(vis) + + elif ch in (curses.KEY_DOWN, ord("j")): + vis = filtered if filtered is not None else entries + if vis: + selected = (selected + 1) % len(vis) + + elif ch in (curses.KEY_BACKSPACE, 127, 8, ord("h")): + # Parent directory + if cwd != cwd.parent: + cwd = cwd.parent + os.chdir(cwd) + selected = 0 + filtered = None + query = None + else: + status = "Already at filesystem root" + status_kind = "status_err" + + elif ch in (curses.KEY_ENTER, 10, 13): + vis = filtered if filtered is not None else entries + if not vis: + continue + entry = vis[selected] + if is_parent(entry): + if cwd != cwd.parent: + cwd = cwd.parent + os.chdir(cwd) + selected = 0 + filtered = None + query = None + elif entry.is_dir(follow_symlinks=False): + try: + os.chdir(entry) + cwd = Path.cwd() + selected = 0 + filtered = None + query = None + except PermissionError: + status = f"Permission denied: {entry.name}" + status_kind = "status_err" + except OSError as e: + status = f"Cannot enter {entry.name}: {e}" + status_kind = "status_err" + else: + if entry.is_file(follow_symlinks=False): + argv = file_menu(stdscr, entry, colors) + if argv is not None: + status = run_external(stdscr, argv) + status_kind = "status_ok" if status.startswith("Ran:") else "status_err" + cwd = Path.cwd() + selected = 0 + + elif ch == ord("/"): + q = prompt_line(stdscr, "/ ") + if q is None: + # cancel search + filtered = None + query = None + selected = 0 + else: + query = q + if q == "": + filtered = None + else: + qlow = q.lower() + candidates = [e for e in entries if qlow in e.name.lower()] + if PARENT in entries: + candidates = [PARENT] + [ + e for e in candidates if e != PARENT + ] + filtered = candidates if candidates else [] + + elif ch == ord("."): + show_hidden = not show_hidden + selected = 0 + # Re-apply search if active + if query: + qlow = query.lower() + candidates = [ + e for e in list_entries(cwd, show_hidden) if qlow in e.name.lower() + ] + filtered = candidates if candidates else [] + + except KeyboardInterrupt: + break + + +def entrypoint(): + try: + curses.wrapper(main) + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + entrypoint() diff --git a/prg/funny/SJNet_2038_timer.py b/prg/funny/SJNet_2038_timer.py new file mode 100644 index 0000000..5e2b689 --- /dev/null +++ b/prg/funny/SJNet_2038_timer.py @@ -0,0 +1,22 @@ +import time +from datetime import datetime, timezone + +ziel = datetime(2038, 1, 19, 3, 14, 7, tzinfo=timezone.utc) + +while True: + jetzt = datetime.now(timezone.utc) + rest = ziel - jetzt + + tage = rest.days + stunden = rest.seconds // 3600 + minuten = (rest.seconds % 3600) // 60 + sekunden = rest.seconds % 60 + + print( + f"\rY2038 Countdown: {tage} Tage " + f"{stunden:02d}:{minuten:02d}:{sekunden:02d}", + end="", + flush=True + ) + + time.sleep(1) diff --git a/prg/funny/SJNet_2038_timer2.py b/prg/funny/SJNet_2038_timer2.py new file mode 100644 index 0000000..5e2b689 --- /dev/null +++ b/prg/funny/SJNet_2038_timer2.py @@ -0,0 +1,22 @@ +import time +from datetime import datetime, timezone + +ziel = datetime(2038, 1, 19, 3, 14, 7, tzinfo=timezone.utc) + +while True: + jetzt = datetime.now(timezone.utc) + rest = ziel - jetzt + + tage = rest.days + stunden = rest.seconds // 3600 + minuten = (rest.seconds % 3600) // 60 + sekunden = rest.seconds % 60 + + print( + f"\rY2038 Countdown: {tage} Tage " + f"{stunden:02d}:{minuten:02d}:{sekunden:02d}", + end="", + flush=True + ) + + time.sleep(1) diff --git a/server.sh b/server.sh new file mode 100755 index 0000000..b30c9f7 --- /dev/null +++ b/server.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +INTERNET_TEST="https://google.com" +HOST="https://serverjonas.de" +SOUND="/home/jonas/Downloads/eb79fa9a-5d7a-42da-9f00-8d6160f96a62.mp3" + +PLAYER_PID="" + +while true; do + # Erst prüfen, ob überhaupt Internet vorhanden ist + if ! curl --silent --fail --max-time 3 "$INTERNET_TEST" > /dev/null; then + echo "$(date): Kein Internet auf diesem Rechner." + + # Falls Alarm läuft, stoppen + if [[ -n "$PLAYER_PID" ]] && kill -0 "$PLAYER_PID" 2>/dev/null; then + kill "$PLAYER_PID" + PLAYER_PID="" + echo "Alarm gestoppt." + fi + + sleep 10 + continue + fi + + # Internet vorhanden -> Server prüfen + if curl --silent --fail --max-time 3 "$HOST" > /dev/null; then + echo "$(date): $HOST erreichbar." + + if [[ -n "$PLAYER_PID" ]] && kill -0 "$PLAYER_PID" 2>/dev/null; then + kill "$PLAYER_PID" + PLAYER_PID="" + echo "Alarm gestoppt." + fi + else + echo "$(date): $HOST NICHT erreichbar!" + + if [[ -z "$PLAYER_PID" ]] || ! kill -0 "$PLAYER_PID" 2>/dev/null; then + ffplay -nodisp -loop 0 "$SOUND" >/dev/null 2>&1 & + PLAYER_PID=$! + echo "Alarm gestartet." + fi + fi + + sleep 10 +done diff --git a/sjnet-launcher b/sjnet-launcher new file mode 100755 index 0000000..7907f7f --- /dev/null +++ b/sjnet-launcher @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + + +#importe +from shutil import get_terminal_size +import os +import sys +import subprocess +import platform + +#helper funktions +def line(content=None): + print("-"*int(get_terminal_size().columns)) + if content is not None: + print(content) + print("-"*int(get_terminal_size().columns)) + +def box(content): + content_lines = content.split("\n") + width = max(len(x) for x in content_lines) + + print("+" + "-" * width + "+") + + for text in content_lines: + print("|" + text.ljust(width) + "|") + + print("+" + "-"* width + "+") + +path = os.path.join(os.path.dirname(__file__), "prg") + + +programs = {} + +for root, dirs, files in os.walk(path): + matching = [] + + for f in files: + if f.startswith("SJNet_") and f.endswith(".py"): + matching.append(f) + + if matching: + folder = os.path.relpath(root, path) + + if folder == ".": + folder = "" + + programs[folder] = matching + +def detect(): + try: + distro = platform.freedesktop_os_release() + except: + return + + name = distro.get("NAME", "").lower() + + if "arch" in name: + box("Arch Linux detected.\nRespect. You chose the hard mode.") + elif "gentoo" in name: + box("Gentoo detected.\nThe compiler has awakened.") + elif "debian" in name: + box("Debian detected.\nStable mode activated.") + elif "ubuntu" in name: + box("Ubuntu detected.\nThe beginner gateway has opened.") + elif os.geteuid() == 0: + box("Running as root.\nPlease don't delete the universe.") + +#main + + +def menu(): + detect() + + if not programs: + box("Keine SJNet_*.py Programme gefunden.") + sys.exit(1) + + + box("Available programs:") + + counter = 1 + program_map = {} + + for folder, files in programs.items(): + + if folder: + print(folder) + + for file in files: + print(f" {counter}: {file}") + + program_map[counter] = os.path.join( + path, + folder, + file + ) + + counter += 1 + + else: + for file in files: + print(f"{counter}: {file}") + + program_map[counter] = os.path.join( + path, + file + ) + + counter += 1 + + try: + choose = int(input("enter number of the programm you want: > ")) + chosen = program_map[choose] + + except ValueError: + print("Es muss eine Nummer sein.") + sys.exit(1) + + except KeyError: + print("Es muss eine der angebotenen Nummern sein.") + sys.exit(1) + + print("\n") + try: + subprocess.run([sys.executable, chosen]) + except KeyboardInterrupt: + print("Interrupt signal recived...") + sys.exit(0) + + + +menu() diff --git a/test.wav b/test.wav new file mode 100644 index 0000000..9f02fad Binary files /dev/null and b/test.wav differ diff --git a/test2.wav b/test2.wav new file mode 100644 index 0000000..911d04c Binary files /dev/null and b/test2.wav differ diff --git a/test3.wav b/test3.wav new file mode 100644 index 0000000..9e54aee Binary files /dev/null and b/test3.wav differ diff --git a/test_llm.py b/test_llm.py new file mode 100644 index 0000000..d11bfa7 --- /dev/null +++ b/test_llm.py @@ -0,0 +1,13 @@ +import ollama + +response = ollama.chat( + model="qwen3:0.6b", + messages=[ + { + "role": "user", + "content": "Hallo, wer bist du?" + } + ] +) + +print(response["message"]["content"]) diff --git a/test_stt.py b/test_stt.py new file mode 100644 index 0000000..d563dc6 --- /dev/null +++ b/test_stt.py @@ -0,0 +1,17 @@ +from faster_whisper import WhisperModel + +model = WhisperModel( + "small", + device="cpu", + compute_type="int8" +) + +segments, info = model.transcribe( + "test.wav", + language="de" +) + +print("Sprache:", info.language) + +for segment in segments: + print(segment.text) diff --git a/todo/Cargo.lock b/todo/Cargo.lock new file mode 100644 index 0000000..fc9e1b9 --- /dev/null +++ b/todo/Cargo.lock @@ -0,0 +1,646 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "todo" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "dirs", + "rusqlite", + "shell-words", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/todo/Cargo.toml b/todo/Cargo.toml new file mode 100644 index 0000000..8543081 --- /dev/null +++ b/todo/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "todo" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = { version = "0.4", features = ["clock"] } +clap = { version = "4", features = ["derive"] } +dirs = "6" +rusqlite = { version = "0.37", features = ["bundled"] } +shell-words = "1" diff --git a/todo/src/main.rs b/todo/src/main.rs new file mode 100644 index 0000000..0c88245 --- /dev/null +++ b/todo/src/main.rs @@ -0,0 +1,307 @@ +use chrono::Local; +use clap::{Parser, Subcommand}; +use rusqlite::{params, Connection}; +use std::io::{self, Write}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command( + name = "todo", + version, + about = "A simple SQLite-based todo manager" +)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Command { + /// Add a new todo + Add { + /// Text of the todo + text: String, + }, + + /// List all todos + List, + + /// Delete a todo by number or exact text + Del { + /// Todo number or exact todo text + target: String, + }, +} + +fn database_path() -> Result> { + let home = dirs::home_dir() + .ok_or("Could not determine home directory")?; + + let directory = home.join(".sjn").join("todo"); + + std::fs::create_dir_all(&directory)?; + + Ok(directory.join("todo.db")) +} + +fn open_database() -> Result> { + let path = database_path()?; + let connection = Connection::open(path)?; + + connection.execute( + " + CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL, + added_at TEXT NOT NULL + ) + ", + [], + )?; + + Ok(connection) +} + +fn add_todo( + connection: &Connection, + text: &str, +) -> Result<(), Box> { + let text = text.trim(); + + if text.is_empty() { + println!("Error: todo text cannot be empty."); + return Ok(()); + } + + let timestamp = Local::now().to_rfc3339(); + + connection.execute( + "INSERT INTO todos (text, added_at) VALUES (?1, ?2)", + params![text, timestamp], + )?; + + let id = connection.last_insert_rowid(); + + println!("Todo #{} added: {}", id, text); + + Ok(()) +} + +fn list_todos( + connection: &Connection, +) -> Result<(), Box> { + let mut statement = connection.prepare( + " + SELECT id, text, added_at + FROM todos + ORDER BY id + ", + )?; + + let todos = statement.query_map([], |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + )) + })?; + + let mut found = false; + + for todo in todos { + let (id, text, added_at) = todo?; + + found = true; + + println!("#{} {} [{}]", id, text, added_at); + } + + if !found { + println!("No todos."); + } + + Ok(()) +} + +fn delete_todo( + connection: &Connection, + target: &str, +) -> Result<(), Box> { + let target = target.trim(); + + if target.is_empty() { + println!("Error: no todo specified."); + return Ok(()); + } + + // Try interpreting the target as a todo number first. + if let Ok(id) = target.parse::() { + let affected = connection.execute( + "DELETE FROM todos WHERE id = ?1", + params![id], + )?; + + if affected == 0 { + println!("Error: todo #{} does not exist.", id); + } else { + println!("Todo #{} deleted.", id); + } + + return Ok(()); + } + + // Otherwise delete by exact text. + let affected = connection.execute( + "DELETE FROM todos WHERE text = ?1", + params![target], + )?; + + if affected == 0 { + println!("Error: no todo with that text exists."); + } else { + println!("Todo deleted: {}", target); + } + + Ok(()) +} + +fn execute_command( + connection: &Connection, + input: &str, +) -> Result> { + let input = input.trim(); + + if input.is_empty() { + return Ok(true); + } + + if input.eq_ignore_ascii_case("exit") + || input.eq_ignore_ascii_case("quit") + { + return Ok(false); + } + + if input.eq_ignore_ascii_case("help") { + println!(); + println!("Commands:"); + println!(" add Add a todo"); + println!(" list List all todos"); + println!(" del Delete todo by number"); + println!(" del Delete todo by exact text"); + println!(" help Show this help"); + println!(" exit / quit Exit the shell"); + println!(); + + return Ok(true); + } + + let args = match shell_words::split(input) { + Ok(args) => args, + Err(error) => { + println!("Error parsing command: {}", error); + return Ok(true); + } + }; + + if args.is_empty() { + return Ok(true); + } + + let command = args[0].to_lowercase(); + + match command.as_str() { + "add" => { + if args.len() < 2 { + println!("Usage: add "); + } else { + let text = args[1..].join(" "); + add_todo(connection, &text)?; + } + } + + "list" => { + if args.len() > 1 { + println!("Usage: list"); + } else { + list_todos(connection)?; + } + } + + "del" | "delete" | "remove" => { + if args.len() < 2 { + println!("Usage: del or del "); + } else { + let target = args[1..].join(" "); + delete_todo(connection, &target)?; + } + } + + _ => { + println!("Unknown command: {}", args[0]); + println!("Type \"help\" for help."); + } + } + + Ok(true) +} + +fn shell( + connection: &Connection, +) -> Result<(), Box> { + println!("Todo Shell"); + println!("Type \"help\" for help."); + println!(); + + loop { + print!("todo> "); + io::stdout().flush()?; + + let mut input = String::new(); + + match io::stdin().read_line(&mut input) { + Ok(0) => { + println!(); + break; + } + + Ok(_) => { + if !execute_command(connection, &input)? { + break; + } + } + + Err(error) => { + eprintln!("Error reading input: {}", error); + break; + } + } + } + + Ok(()) +} + +fn main() -> Result<(), Box> { + let connection = open_database()?; + + let cli = Cli::parse(); + + match cli.command { + Some(Command::Add { text }) => { + add_todo(&connection, &text)?; + } + + Some(Command::List) => { + list_todos(&connection)?; + } + + Some(Command::Del { target }) => { + delete_todo(&connection, &target)?; + } + + None => { + shell(&connection)?; + } + } + + Ok(()) +} diff --git a/uuid_test.py b/uuid_test.py new file mode 100644 index 0000000..797603a --- /dev/null +++ b/uuid_test.py @@ -0,0 +1,71 @@ +import sqlite3 +import uuid +import math +import time + +DB = "uuid_test.db" + +# SQLite-Datenbank öffnen/erstellen +conn = sqlite3.connect(DB) +cursor = conn.cursor() + +cursor.execute(""" +CREATE TABLE IF NOT EXISTS uuids ( + uuid TEXT PRIMARY KEY +) +""") +conn.commit() + +n = 0 +start = time.time() + +# UUIDv4 hat 122 zufällige Bits +N = 2**122 + +print("UUIDv4-Kollisionstest gestartet!") +print("Strg+C zum Beenden.\n") + +try: + while True: + new_uuid = str(uuid.uuid4()) + n += 1 + + # Einfügen versuchen + try: + cursor.execute( + "INSERT INTO uuids (uuid) VALUES (?)", + (new_uuid,) + ) + conn.commit() + + duplicate = False + + except sqlite3.IntegrityError: + # UUID existiert bereits + duplicate = True + + # Kollisionswahrscheinlichkeit + probability = 1 - math.exp( + -(n * (n - 1)) / (2 * N) + ) + + print( + f"\rUUIDs: {n:,} | " + f"Letzte: {new_uuid} | " + f"Doppelt: {'JA' if duplicate else 'NEIN'} | " + f"Kolliw.: {probability:.12e}", + end="", + flush=True + ) + + if duplicate: + print("\n\n!!! KOLLISION GEFUNDEN !!!") + print(f"UUID: {new_uuid}") + print(f"Nach {n:,} Versuchen") + break + +except KeyboardInterrupt: + print("\n\nTest beendet.") + +finally: + conn.close()