initialise project

This commit is contained in:
2026-08-23 13:03:06 +02:00
commit 2dfdf54d42
22 changed files with 3169 additions and 0 deletions
+246
View File
@@ -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/
+702
View File
@@ -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()
+2
View File
@@ -0,0 +1,2 @@
zsh: command not found: dig
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env python3
print("test")
print("main.py wurde gestartet")
+46
View File
@@ -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.
+75
View File
@@ -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))
+159
View File
@@ -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<len(self.table[gi]): raise ValueError('Teilposition ungültig.')
self.save(); x=self.table[gi]; self.table[gi]=x[:pos]; self.table.insert(gi+1,x[pos:])
def draw(self):
if self.drew: raise ValueError('Du hast bereits gezogen.')
if not self.deck: raise ValueError('Stapel leer.')
self.save(); self.p.hand.append(self.deck.pop()); self.p.hand=sort_hand(self.p.hand); self.drew=True
def finish(self):
if any(not g or not valid(g, self.wrap) for g in self.table):
print('Zug ungültig: Es gibt eine ungültige Tischgruppe.')
return False
if not self.p.opened:
if self.min30:
total=sum(sum(points(c) for c in g) for g in self.table)
if total<30:
print(f'Erstmeldung braucht mindestens 30 Punkte. Aktuell: {total}')
return False
self.p.opened=True
if not self.p.hand:
print(f'\n{self.p.name} gewinnt!')
return True
self.turn=1-self.turn; self.drew=False; self.history=[]
self.handover=True
return False
def command(self,line):
a=line.split()
if not a:return False
try:
cmd=a[0].lower()
if cmd=='m' and len(a)==3:self.move_hand(a[1],a[2])
elif cmd=='t' and len(a)==3:self.move_table(a[1],a[2])
elif cmd=='n':self.save();self.table.append([])
elif cmd=='a' and len(a)==3:self.merge(a[1],a[2])
elif cmd=='s' and len(a)==3:self.split(a[1],a[2])
elif cmd=='d':self.draw()
elif cmd=='u':self.undo()
elif cmd=='sort':self.p.hand=sort_hand(self.p.hand)
elif cmd=='finish':return self.finish()
elif cmd in ('q','quit','exit'):raise SystemExit
elif cmd=='show':pass
else: raise ValueError('Unbekannter Befehl.')
except (ValueError,IndexError) as e: print('Fehler:',e)
input('\nENTER...')
return False
def run(self):
while True:
self.show()
if self.command(input('\n> ')): 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...')
+4
View File
@@ -0,0 +1,4 @@
from game import Game
if __name__ == '__main__':
Game().run()
+646
View File
@@ -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()
+22
View File
@@ -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)
+22
View File
@@ -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)
Executable
+45
View File
@@ -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
Executable
+132
View File
@@ -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()
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+13
View File
@@ -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"])
+17
View File
@@ -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)
+646
View File
@@ -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",
]
+11
View File
@@ -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"
+307
View File
@@ -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<Command>,
}
#[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<PathBuf, Box<dyn std::error::Error>> {
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<Connection, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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::<i64>() {
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<bool, Box<dyn std::error::Error>> {
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 <text> Add a todo");
println!(" list List all todos");
println!(" del <number> Delete todo by number");
println!(" del <text> 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 <text>");
} 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 <number> or del <text>");
} 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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}
+71
View File
@@ -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()